From ca955c34aba0b9a7bc8a22c14a8ab66229ef9736 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 12:44:56 +0000 Subject: [PATCH 01/11] Modernize CI, update dependencies, and add security policy Replaces Travis CI with GitHub Actions workflows, adds CodeQL analysis, and introduces Dependabot configuration for automated dependency updates. Updates Python support to 3.8+ in setup.py and pyproject.toml, modernizes pre-commit hooks, and expands .gitignore for better cross-platform and tool coverage. Adds SECURITY.md and .python-version, removes .travis.yml, and makes minor docstring and formatting improvements throughout the codebase. --- .editorconfig | 2 +- .flake8 | 2 +- .gitattribute | 2 +- .github/dependabot.yml | 33 ++++++++++ .github/workflows/codeql.yml | 40 ++++++++++++ .github/workflows/test.yml | 50 +++++++++----- .gitignore | 65 ++++++++++++++++++- .isort.cfg | 2 +- .pre-commit-config.yaml | 20 +++++- .pylintrc | 2 +- .python-version | 1 + .travis.yml | 19 ------ README.rst | 10 +-- SECURITY.md | 47 ++++++++++++++ pyproject.toml | 4 +- setup.py | 8 ++- tilingsgui/__init__.py | 3 +- tilingsgui/app.py | 6 +- tilingsgui/events.py | 3 +- tilingsgui/files.py | 3 +- tilingsgui/geometry.py | 3 +- tilingsgui/graphics.py | 4 +- tilingsgui/main.py | 3 +- tilingsgui/menu.py | 4 +- tilingsgui/resources/img/svg/add_custom.svg | 2 +- tilingsgui/resources/img/svg/add_point.svg | 2 +- tilingsgui/resources/img/svg/export.svg | 2 +- tilingsgui/resources/img/svg/factor.svg | 2 +- tilingsgui/resources/img/svg/factor_int.svg | 2 +- tilingsgui/resources/img/svg/fusion_c.svg | 4 +- .../resources/img/svg/fusion_comp_c.svg | 4 +- .../resources/img/svg/fusion_comp_r.svg | 4 +- tilingsgui/resources/img/svg/fusion_r.svg | 4 +- tilingsgui/resources/img/svg/htc.svg | 2 +- tilingsgui/resources/img/svg/move.svg | 2 +- tilingsgui/resources/img/svg/obs_inf.svg | 2 +- tilingsgui/resources/img/svg/obstr_trans.svg | 2 +- tilingsgui/resources/img/svg/place_east.svg | 2 +- tilingsgui/resources/img/svg/place_north.svg | 2 +- tilingsgui/resources/img/svg/place_south.svg | 2 +- tilingsgui/resources/img/svg/place_west.svg | 2 +- tilingsgui/resources/img/svg/pplace_east.svg | 2 +- tilingsgui/resources/img/svg/pplace_north.svg | 2 +- tilingsgui/resources/img/svg/pplace_south.svg | 2 +- tilingsgui/resources/img/svg/pplace_west.svg | 2 +- tilingsgui/resources/img/svg/pretty.svg | 2 +- tilingsgui/resources/img/svg/redo.svg | 2 +- tilingsgui/resources/img/svg/rowcolsep.svg | 2 +- tilingsgui/resources/img/svg/shading.svg | 6 +- tilingsgui/resources/img/svg/show_cross.svg | 2 +- tilingsgui/resources/img/svg/show_local.svg | 2 +- tilingsgui/resources/img/svg/str.svg | 2 +- tilingsgui/resources/img/svg/tikz.svg | 2 +- tilingsgui/resources/img/svg/undo.svg | 2 +- tilingsgui/resources/img/svg/verification.svg | 2 +- tilingsgui/state.py | 3 +- tilingsgui/tplot.py | 10 ++- tilingsgui/utils.py | 3 +- tilingsgui/widgets.py | 3 +- tox.ini | 28 ++++---- 60 files changed, 321 insertions(+), 136 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .python-version delete mode 100644 .travis.yml create mode 100644 SECURITY.md diff --git a/.editorconfig b/.editorconfig index d0d5825..68cec78 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,4 +12,4 @@ indent_size = 4 indent_size = 2 [*.{md,rst}] -trim_trailing_whitespace = false \ No newline at end of file +trim_trailing_whitespace = false diff --git a/.flake8 b/.flake8 index e0ea542..8dd399a 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 88 -extend-ignore = E203 \ No newline at end of file +extend-ignore = E203 diff --git a/.gitattribute b/.gitattribute index 71027c2..478b946 100644 --- a/.gitattribute +++ b/.gitattribute @@ -18,4 +18,4 @@ LICENSE text eol=lf .gitignore text eol=lf .gitattribute text eol=lf .flake8 text eol=lf -.pylintrc text eol=lf \ No newline at end of file +.pylintrc text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..680f85b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + # Enable version updates for Python dependencies + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 10 + reviewers: + - "PermutaTriangle" + assignees: + - "PermutaTriangle" + commit-message: + prefix: "deps" + include: "scope" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 5 + reviewers: + - "PermutaTriangle" + assignees: + - "PermutaTriangle" + commit-message: + prefix: "ci" + include: "scope" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e476994 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: "CodeQL" + +on: + push: + branches: [ master, develop ] + pull_request: + branches: [ master ] + schedule: + - cron: '30 2 * * 1' # Weekly on Mondays at 02:30 UTC + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 691a48b..50ec74e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,22 +9,21 @@ jobs: fail-fast: false matrix: include: - - python: 3.8 + # Linting and type checking (run on Python 3.11) + - python: "3.11" toxenv: flake8 os: ubuntu-latest - - python: 3.8 + - python: "3.11" toxenv: mypy os: ubuntu-latest - - python: 3.8 + - python: "3.11" toxenv: pylint os: ubuntu-latest - - python: 3.8 + - python: "3.11" toxenv: black os: ubuntu-latest - - python: 3.7 - toxenv: py37 - os: ubuntu-latest + # Python version testing (Linux) - python: 3.8 toxenv: py38 os: ubuntu-latest @@ -34,23 +33,40 @@ jobs: - python: "3.10" toxenv: py310 os: ubuntu-latest - - python: pypy-3.7 - toxenv: pypy37 + - python: "3.11" + toxenv: py311 + os: ubuntu-latest + - python: "3.12" + toxenv: py312 + os: ubuntu-latest + - python: "3.13" + toxenv: py313 + os: ubuntu-latest + - python: pypy-3.8 + toxenv: pypy38 + os: ubuntu-latest + - python: pypy-3.9 + toxenv: pypy39 + os: ubuntu-latest + - python: pypy-3.10 + toxenv: pypy310 os: ubuntu-latest - - python: 3.8 - toxenv: py38 + # Cross-platform testing (Python 3.11) + - python: "3.11" + toxenv: py311 os: macos-latest - - python: 3.8 - toxenv: py38 + - python: "3.11" + toxenv: py311 os: windows-latest runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python }} + python-version: ${{ matrix.python }} + allow-prereleases: true - name: install dependencies run: python -m pip install --upgrade pip tox - name: run @@ -58,4 +74,4 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox - name: setup - run: python setup.py install \ No newline at end of file + run: python setup.py install diff --git a/.gitignore b/.gitignore index 20229bc..4eac1e6 100644 --- a/.gitignore +++ b/.gitignore @@ -86,8 +86,8 @@ target/ profile_default/ ipython_config.py -# pyenv -.python-version +# pyenv (but we want to track our .python-version file) +# .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -142,4 +142,63 @@ dmypy.json .vscode/ # Program's output -tilingsgui/exports/ \ No newline at end of file +tilingsgui/exports/ +exports/ + +# macOS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Windows +*.stackdump + +# Linux +*~ + +# Temporary/backup files +*.tmp +*.temp +*.bak +*.swp +*.swo +*~ + +# IDE specific files (additional) +.idea/ +*.iml +*.ipr +*.iws +.vscode/settings.json +.vscode/launch.json +.vscode/tasks.json + +# Pre-commit +.pre-commit-config.yaml.bak + +# Ruff cache (modern Python linter) +.ruff_cache/ + +# Modern type checkers +.pyright/ + +# Coverage.py +.coverage.* +coverage.xml +htmlcov/ + +# Duplicate files (with version numbers or " 2" suffix) +*" 2".* +* 2.* + +# Temporary export files +*.json.tmp +tilings_export.json + +# Development files that shouldn't be committed +hamstur.py +2025-MODERNIZATION-PLAN.md diff --git a/.isort.cfg b/.isort.cfg index 3884dca..92c8e2d 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -5,4 +5,4 @@ multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True -line_length=88 \ No newline at end of file +line_length=88 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee9075b..1df3071 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,21 @@ repos: - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 24.8.0 hooks: - - id: black \ No newline at end of file + - id: black + language_version: python3.11 + + - repo: https://github.com/pycqa/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + additional_dependencies: [flake8-isort] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict diff --git a/.pylintrc b/.pylintrc index e1de93b..55a6ac1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -5,4 +5,4 @@ init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.app max-args=10 max-module-lines=1200 disable=too-few-public-methods,too-many-lines -good-names=r,c,x,y,h,w,i,j,k,n,x1,x2,y1,y2,g,b,dx,dy \ No newline at end of file +good-names=r,c,x,y,h,w,i,j,k,n,x1,x2,y1,y2,g,b,dx,dy diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6210168..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: python -matrix: - include: - - python: 3.8 - env: TOXENV=flake8 -install: -- pip install tox -script: -- tox -deploy: - skip_cleanup: true - provider: pypi - user: __token__ - password: - secure: ZEsM76obFP9ZIGdHkKIgMFc/7qN33ZCO/2Tl0E8noUOHuL/dPkKeG1VWD7XiXupljULgRkxKInrz5aw/423+qwy1kQw2rDxdxbs4ITAbp3xEd4X7N2WwvYp7biuw5z4I2lzY7TI6HvHxFeN25Wvo1qIc7SIMMcYmSAY7pOs7qW+ljfrg9PhBJZBk0zZ3ISAu8npkIVa9FKaYTbx0lq9GWdy3bovkGER4DwcjGB6Cx0OCSrLxwYqNi34xlSy7hBQugQp85SqSlwOpgxLGgxr0/xyW73iQdoQhcFdl3Aoupudu/suy0w7JShANGeD2cWtXs5a028w79tKwKmlb11gSFbLT9fdhxdSPU87qm5EOye0loCg5pGQtZvZ2Zf+WyIFaPWl7UewcBDHNSx2APUHokpI9N6qoaI94CcY5UHCmlXGc2PMN00WmhTlHGYsFEADoFa5brqBNEvP5XpHa8eT/w63tGG/v/a9NdfLMzktYemUIGdNZrOj8V1YGjd28HsxE2/zF/kG9Qbq1878tYmocpxro8gQXdbzc1DyiXZsyDWPRSv7WCIXl07Y2nWIqKuuQmmrFlxTw+SmRiIXpzl2YZ2c5UqPJFNcaJ5fZ2U+MwadbkhuANIYn5vc4AkZyz2L/jeyKz7wNB7Ar8DgAKxKkZ0gJv7AICZTGEXXEtIeMsdk= - skip_existing: true - on: - branch: master - condition: "$TRAVIS_OS_NAME = linux" diff --git a/README.rst b/README.rst index ac5c932..736c34f 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ Without them the app still works but pasting won’t. Known issues ------------ -* +* Report a bug ~~~~~~~~~~~~ @@ -79,9 +79,9 @@ The following two inputs are two ways of producing the same initial tiling. .. code:: 1432_12345 - + {"class_module": "tilings.tiling", "comb_class": "Tiling", "obstructions": [{"patt": [0, 3, 2, 1], "pos": [[0, 0], [0, 0], [0, 0], [0, 0]]}, {"patt": [0, 1, 2, 3, 4], "pos": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]}], "requirements": [], "assumptions": []} - + The initial tiling in question would be the following. .. code:: sh @@ -90,7 +90,7 @@ The initial tiling in question would be the following. |1| +-+ 1: Av(0321, 01234) - + Cell insertion ~~~~~~~~~~~~~~ @@ -131,7 +131,7 @@ By clicking a point of a requirement, we pass its gridded permutation along with Fusion ~~~~~~ -Let ``c_r`` and ``c_c`` be the row and column respectively of the clicked cell. There are 4 types of fusions available. Fusion with ``row=c_r``, |fusion_r|, fusion with ``col=c_c``, |fusion_c|, component fusion with ``row=c_r``, |fusion_comp_r|, and component fusion with ``col=c_c``, |fusion_comp_c|. If the fusion are invalid, then exceptions are caught and nothing happens. +Let ``c_r`` and ``c_c`` be the row and column respectively of the clicked cell. There are 4 types of fusions available. Fusion with ``row=c_r``, |fusion_r|, fusion with ``col=c_c``, |fusion_c|, component fusion with ``row=c_r``, |fusion_comp_r|, and component fusion with ``col=c_c``, |fusion_comp_c|. If the fusion are invalid, then exceptions are caught and nothing happens. Fusion: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e5b7775 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,47 @@ +# Security Policy + +## Supported Versions + +We actively support the following versions of TilingsGUI with security updates: + +| Version | Supported | +| ------- | ------------------ | +| 0.2.x | :white_check_mark: | +| < 0.2 | :x: | + +## Reporting a Vulnerability + +If you discover a security vulnerability in TilingsGUI, please report it to us privately. We take security seriously and will respond promptly to legitimate security concerns. + +### How to Report + +1. **Email**: Send an email to permutatriangle@gmail.com with the subject line "Security Vulnerability in TilingsGUI" +2. **Include**: + - A detailed description of the vulnerability + - Steps to reproduce the issue + - Potential impact assessment + - Any suggested fixes (if available) + +### What to Expect + +- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours +- **Initial Assessment**: We will provide an initial assessment within 5 business days +- **Resolution Timeline**: We aim to resolve critical security issues within 30 days +- **Disclosure**: We will coordinate with you on appropriate disclosure timing + +### Security Best Practices + +When using TilingsGUI: +- Keep your installation up to date with the latest version +- Only load tilings from trusted sources +- Be cautious when running TilingsGUI with elevated privileges +- Report any suspicious behavior or unexpected security prompts + +## Scope + +This security policy covers: +- The TilingsGUI application itself +- Dependencies and third-party libraries +- Build and deployment processes + +Thank you for helping keep TilingsGUI secure! diff --git a/pyproject.toml b/pyproject.toml index f2d5bca..d184c0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ['py37'] +target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313'] include = '\.pyi?$' exclude = ''' ( @@ -18,4 +18,4 @@ exclude = ''' | foo.py # also separately exclude a file named foo.py in # the root of the project ) -''' \ No newline at end of file +''' diff --git a/setup.py b/setup.py index 522586d..e5bb0da 100644 --- a/setup.py +++ b/setup.py @@ -31,8 +31,8 @@ def get_version(): }, packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), long_description=read("README.rst"), - install_requires=["pyperclip==1.8.1", "pyglet==1.5.15", "tilings==2.5.0"], - python_requires=">=3.6", + install_requires=["pyperclip>=1.9.0", "pyglet>=1.5.15,<2.0", "tilings>=2.5.0"], + python_requires=">=3.8", include_package_data=True, classifiers=[ "Topic :: Education", @@ -41,10 +41,12 @@ def get_version(): "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], diff --git a/tilingsgui/__init__.py b/tilingsgui/__init__.py index af7642d..bb16f6e 100644 --- a/tilingsgui/__init__.py +++ b/tilingsgui/__init__.py @@ -1,4 +1,3 @@ -"""As the tilingsgui is not intened to be imported, this only holds the version. -""" +"""As the tilingsgui is not intened to be imported, this only holds the version.""" __version__ = "0.2.3" diff --git a/tilingsgui/app.py b/tilingsgui/app.py index d425915..6b321ca 100644 --- a/tilingsgui/app.py +++ b/tilingsgui/app.py @@ -26,9 +26,9 @@ class TilingGui(pyglet.window.Window): _INITIAL_HEIGHT: ClassVar[int] = 650 _RIGHT_BAR_WIDTH: ClassVar[int] = 200 _TOP_BAR_HEIGHT: ClassVar[int] = 24 - _CLEAR_COLOR: ClassVar[ - Tuple[float, float, float, float] - ] = Color.alpha_extend_and_scale_to_01(Color.WHITE) + _CLEAR_COLOR: ClassVar[Tuple[float, float, float, float]] = ( + Color.alpha_extend_and_scale_to_01(Color.WHITE) + ) def __init__(self, init_tiling: str, *args, **kargs) -> None: """Instantiate the parent window class and create all diff --git a/tilingsgui/events.py b/tilingsgui/events.py index fcb3aec..b70ade1 100644 --- a/tilingsgui/events.py +++ b/tilingsgui/events.py @@ -1,5 +1,4 @@ -"""Event related module. -""" +"""Event related module.""" from typing import Iterable diff --git a/tilingsgui/files.py b/tilingsgui/files.py index 5f73c4d..dd5590b 100644 --- a/tilingsgui/files.py +++ b/tilingsgui/files.py @@ -1,5 +1,4 @@ -"""A collection of file and path related functionality. -""" +"""A collection of file and path related functionality.""" import json import pathlib diff --git a/tilingsgui/geometry.py b/tilingsgui/geometry.py index af33417..4c013ec 100644 --- a/tilingsgui/geometry.py +++ b/tilingsgui/geometry.py @@ -1,5 +1,4 @@ -"""Mathematical geometric objects. -""" +"""Mathematical geometric objects.""" from typing import Iterator diff --git a/tilingsgui/graphics.py b/tilingsgui/graphics.py index 499b501..9b99012 100644 --- a/tilingsgui/graphics.py +++ b/tilingsgui/graphics.py @@ -1,6 +1,4 @@ -"""Drawable objects -""" - +"""Drawable objects""" from math import cos, pi, sin from typing import ClassVar, List, Tuple diff --git a/tilingsgui/main.py b/tilingsgui/main.py index 39168ff..119b19d 100644 --- a/tilingsgui/main.py +++ b/tilingsgui/main.py @@ -1,5 +1,4 @@ -"""Entrypoint. -""" +"""Entrypoint.""" import argparse diff --git a/tilingsgui/menu.py b/tilingsgui/menu.py index 59186c6..a943e0e 100644 --- a/tilingsgui/menu.py +++ b/tilingsgui/menu.py @@ -1,6 +1,4 @@ -"""Control stations. -""" - +"""Control stations.""" from typing import Iterable diff --git a/tilingsgui/resources/img/svg/add_custom.svg b/tilingsgui/resources/img/svg/add_custom.svg index 06edd7e..9d145e8 100644 --- a/tilingsgui/resources/img/svg/add_custom.svg +++ b/tilingsgui/resources/img/svg/add_custom.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/add_point.svg b/tilingsgui/resources/img/svg/add_point.svg index c58fd13..1851242 100644 --- a/tilingsgui/resources/img/svg/add_point.svg +++ b/tilingsgui/resources/img/svg/add_point.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/export.svg b/tilingsgui/resources/img/svg/export.svg index 45ea3c4..1226846 100644 --- a/tilingsgui/resources/img/svg/export.svg +++ b/tilingsgui/resources/img/svg/export.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/factor.svg b/tilingsgui/resources/img/svg/factor.svg index 8aa063e..6fca35c 100644 --- a/tilingsgui/resources/img/svg/factor.svg +++ b/tilingsgui/resources/img/svg/factor.svg @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/factor_int.svg b/tilingsgui/resources/img/svg/factor_int.svg index 9aa7c3f..280667c 100644 --- a/tilingsgui/resources/img/svg/factor_int.svg +++ b/tilingsgui/resources/img/svg/factor_int.svg @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/fusion_c.svg b/tilingsgui/resources/img/svg/fusion_c.svg index 6fe2189..673e3e0 100644 --- a/tilingsgui/resources/img/svg/fusion_c.svg +++ b/tilingsgui/resources/img/svg/fusion_c.svg @@ -7,5 +7,5 @@ - - \ No newline at end of file + + diff --git a/tilingsgui/resources/img/svg/fusion_comp_c.svg b/tilingsgui/resources/img/svg/fusion_comp_c.svg index e946d01..ddb27cb 100644 --- a/tilingsgui/resources/img/svg/fusion_comp_c.svg +++ b/tilingsgui/resources/img/svg/fusion_comp_c.svg @@ -8,5 +8,5 @@ - - \ No newline at end of file + + diff --git a/tilingsgui/resources/img/svg/fusion_comp_r.svg b/tilingsgui/resources/img/svg/fusion_comp_r.svg index 6bc84f1..915046c 100644 --- a/tilingsgui/resources/img/svg/fusion_comp_r.svg +++ b/tilingsgui/resources/img/svg/fusion_comp_r.svg @@ -8,5 +8,5 @@ - - \ No newline at end of file + + diff --git a/tilingsgui/resources/img/svg/fusion_r.svg b/tilingsgui/resources/img/svg/fusion_r.svg index 68a7f16..301d89f 100644 --- a/tilingsgui/resources/img/svg/fusion_r.svg +++ b/tilingsgui/resources/img/svg/fusion_r.svg @@ -7,5 +7,5 @@ - - \ No newline at end of file + + diff --git a/tilingsgui/resources/img/svg/htc.svg b/tilingsgui/resources/img/svg/htc.svg index 470225b..f930712 100644 --- a/tilingsgui/resources/img/svg/htc.svg +++ b/tilingsgui/resources/img/svg/htc.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/move.svg b/tilingsgui/resources/img/svg/move.svg index e20cd3a..20fbed8 100644 --- a/tilingsgui/resources/img/svg/move.svg +++ b/tilingsgui/resources/img/svg/move.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/obs_inf.svg b/tilingsgui/resources/img/svg/obs_inf.svg index 62b8dba..ed3c951 100644 --- a/tilingsgui/resources/img/svg/obs_inf.svg +++ b/tilingsgui/resources/img/svg/obs_inf.svg @@ -12,4 +12,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/obstr_trans.svg b/tilingsgui/resources/img/svg/obstr_trans.svg index fa7c688..38f9f57 100644 --- a/tilingsgui/resources/img/svg/obstr_trans.svg +++ b/tilingsgui/resources/img/svg/obstr_trans.svg @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/place_east.svg b/tilingsgui/resources/img/svg/place_east.svg index fa98b92..f0d32e7 100644 --- a/tilingsgui/resources/img/svg/place_east.svg +++ b/tilingsgui/resources/img/svg/place_east.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/place_north.svg b/tilingsgui/resources/img/svg/place_north.svg index fe334d3..fec0bd3 100644 --- a/tilingsgui/resources/img/svg/place_north.svg +++ b/tilingsgui/resources/img/svg/place_north.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/place_south.svg b/tilingsgui/resources/img/svg/place_south.svg index cbd743c..331805c 100644 --- a/tilingsgui/resources/img/svg/place_south.svg +++ b/tilingsgui/resources/img/svg/place_south.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/place_west.svg b/tilingsgui/resources/img/svg/place_west.svg index 819e221..32112c4 100644 --- a/tilingsgui/resources/img/svg/place_west.svg +++ b/tilingsgui/resources/img/svg/place_west.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/pplace_east.svg b/tilingsgui/resources/img/svg/pplace_east.svg index 0e90391..52a538b 100644 --- a/tilingsgui/resources/img/svg/pplace_east.svg +++ b/tilingsgui/resources/img/svg/pplace_east.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/pplace_north.svg b/tilingsgui/resources/img/svg/pplace_north.svg index 136ceeb..dabbe08 100644 --- a/tilingsgui/resources/img/svg/pplace_north.svg +++ b/tilingsgui/resources/img/svg/pplace_north.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/pplace_south.svg b/tilingsgui/resources/img/svg/pplace_south.svg index 3d32f44..a09dbc6 100644 --- a/tilingsgui/resources/img/svg/pplace_south.svg +++ b/tilingsgui/resources/img/svg/pplace_south.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/pplace_west.svg b/tilingsgui/resources/img/svg/pplace_west.svg index 59582dc..22998e7 100644 --- a/tilingsgui/resources/img/svg/pplace_west.svg +++ b/tilingsgui/resources/img/svg/pplace_west.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/pretty.svg b/tilingsgui/resources/img/svg/pretty.svg index 4d27ce1..b97f2bd 100644 --- a/tilingsgui/resources/img/svg/pretty.svg +++ b/tilingsgui/resources/img/svg/pretty.svg @@ -4,4 +4,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/redo.svg b/tilingsgui/resources/img/svg/redo.svg index d905272..c33f9ff 100644 --- a/tilingsgui/resources/img/svg/redo.svg +++ b/tilingsgui/resources/img/svg/redo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/rowcolsep.svg b/tilingsgui/resources/img/svg/rowcolsep.svg index a91f57f..afbaa59 100644 --- a/tilingsgui/resources/img/svg/rowcolsep.svg +++ b/tilingsgui/resources/img/svg/rowcolsep.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/shading.svg b/tilingsgui/resources/img/svg/shading.svg index 9eda0f9..1156ff6 100644 --- a/tilingsgui/resources/img/svg/shading.svg +++ b/tilingsgui/resources/img/svg/shading.svg @@ -1,6 +1,6 @@ - + Sunglass Created with Sketch. @@ -50,8 +50,8 @@ - - + + diff --git a/tilingsgui/resources/img/svg/show_cross.svg b/tilingsgui/resources/img/svg/show_cross.svg index 6ac6d06..c4f8cbb 100644 --- a/tilingsgui/resources/img/svg/show_cross.svg +++ b/tilingsgui/resources/img/svg/show_cross.svg @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/show_local.svg b/tilingsgui/resources/img/svg/show_local.svg index d95aaa5..32d8cc6 100644 --- a/tilingsgui/resources/img/svg/show_local.svg +++ b/tilingsgui/resources/img/svg/show_local.svg @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/str.svg b/tilingsgui/resources/img/svg/str.svg index 3f191c0..4a24983 100644 --- a/tilingsgui/resources/img/svg/str.svg +++ b/tilingsgui/resources/img/svg/str.svg @@ -1,4 +1,4 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/tikz.svg b/tilingsgui/resources/img/svg/tikz.svg index 2284e29..8a31f30 100644 --- a/tilingsgui/resources/img/svg/tikz.svg +++ b/tilingsgui/resources/img/svg/tikz.svg @@ -1,4 +1,4 @@ TIKZ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/undo.svg b/tilingsgui/resources/img/svg/undo.svg index 59d5ef9..77000a4 100644 --- a/tilingsgui/resources/img/svg/undo.svg +++ b/tilingsgui/resources/img/svg/undo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/resources/img/svg/verification.svg b/tilingsgui/resources/img/svg/verification.svg index 774b447..229ea80 100644 --- a/tilingsgui/resources/img/svg/verification.svg +++ b/tilingsgui/resources/img/svg/verification.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tilingsgui/state.py b/tilingsgui/state.py index 5ba5aed..dd33f5c 100644 --- a/tilingsgui/state.py +++ b/tilingsgui/state.py @@ -1,5 +1,4 @@ -"""A global state for the app, in a seperate module. -""" +"""A global state for the app, in a seperate module.""" from typing import Tuple diff --git a/tilingsgui/tplot.py b/tilingsgui/tplot.py index 34612f8..fcf1e59 100644 --- a/tilingsgui/tplot.py +++ b/tilingsgui/tplot.py @@ -1,5 +1,4 @@ -"""The tiling drawing tools. -""" +"""The tiling drawing tools.""" import json from collections import Counter, deque @@ -13,9 +12,8 @@ from tilings import GriddedPerm, Tiling from tilings.algorithms import Factor, FactorWithInterleaving from tilings.exception import InvalidOperationError -from tilings.strategies import ( +from tilings.strategies import ( # DatabaseVerificationStrategy removed v4.0.0 BasicVerificationStrategy, - DatabaseVerificationStrategy, ElementaryVerificationStrategy, InsertionEncodingVerificationStrategy, LocallyFactorableVerificationStrategy, @@ -469,7 +467,7 @@ class TPlotManager(pyglet.event.EventDispatcher, Observer): _MAX_SEQUENCE_SIZE: ClassVar[int] = 7 _VERIFICATION_STRATS: ClassVar[List[str]] = [ "BasicVerificationStrategy", - "DatabaseVerificationStrategy", + # "DatabaseVerificationStrategy", # Removed in tilings 4.0.0 "ElementaryVerificationStrategy", "InsertionEncodingVerificationStrategy", "LocallyFactorableVerificationStrategy", @@ -492,7 +490,7 @@ def _verify(tiling: Tiling) -> List[str]: """ return [ str(BasicVerificationStrategy().verified(tiling)), - str(DatabaseVerificationStrategy().verified(tiling)), + # str(DatabaseVerificationStrategy().verified(tiling)), # Removed str(ElementaryVerificationStrategy().verified(tiling)), str(InsertionEncodingVerificationStrategy().verified(tiling)), str(LocallyFactorableVerificationStrategy().verified(tiling)), diff --git a/tilingsgui/utils.py b/tilingsgui/utils.py index f2b4c3c..8624262 100644 --- a/tilingsgui/utils.py +++ b/tilingsgui/utils.py @@ -1,5 +1,4 @@ -"""A collection of various utility functionality. -""" +"""A collection of various utility functionality.""" import datetime diff --git a/tilingsgui/widgets.py b/tilingsgui/widgets.py index 808b00a..dd04557 100644 --- a/tilingsgui/widgets.py +++ b/tilingsgui/widgets.py @@ -1,5 +1,4 @@ -"""Graphical UI components. -""" +"""Graphical UI components.""" from typing import Callable, ClassVar, Dict, List, Optional, Tuple diff --git a/tox.ini b/tox.ini index 85c2a40..bacb4a6 100644 --- a/tox.ini +++ b/tox.ini @@ -6,23 +6,27 @@ [tox] envlist = flake8, mypy, pylint, black - py{37,38,39,310}, - pypy37 + py{38,39,310,311,312,313}, + pypy{38,39,310} [default] -basepython=python3.8 +basepython=python3.11 [testenv] description = run test basepython = - py37: python3.7 py38: python3.8 py39: python3.9 py310: python3.10 - pypy37: pypy3 + py311: python3.11 + py312: python3.12 + py313: python3.13 + pypy38: pypy3.8 + pypy39: pypy3.9 + pypy310: pypy3.10 deps = - pytest==7.2.0 - pytest-timeout==2.1.0 + pytest>=8.0.0 + pytest-timeout>=2.3.0 commands = pytest [pytest] @@ -34,8 +38,8 @@ description = run flake8 (linter) basepython = {[default]basepython} skip_install = True deps = - flake8==5.0.4 - flake8-isort==5.0.0 + flake8>=7.0.0 + flake8-isort>=6.0.0 commands = flake8 --isort-show-traceback tilingsgui tests setup.py @@ -43,19 +47,19 @@ commands = description = run pylint (static code analysis) basepython = {[default]basepython} deps = - pylint==2.15.5 + pylint>=3.0.0 commands = pylint tilingsgui [testenv:mypy] description = run mypy (static type checker) basepython = {[default]basepython} deps = - mypy==0.990 + mypy>=1.13.0 commands = mypy [testenv:black] description = check that comply with autoformating basepython = {[default]basepython} deps = - black==22.10.0 + black>=24.0.0 commands = black --check --diff . From 29c9f8b804a27a168d98322b960b1448b7fb6a54 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 12:55:31 +0000 Subject: [PATCH 02/11] Update build config and linting, fix widget style dict Added build-system section to pyproject.toml for setuptools and wheel. Updated GitHub Actions workflow to install setuptools separately. Modified tox.ini to include setuptools as a dependency. Improved .pylintrc init-hook and disabled too-many-positional-arguments warning. Fixed style dict usage in widgets.py for pyglet text document. --- .github/workflows/test.yml | 4 +++- .pylintrc | 4 ++-- pyproject.toml | 4 ++++ tilingsgui/widgets.py | 2 +- tox.ini | 1 + 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50ec74e..c93e1eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,7 +68,9 @@ jobs: python-version: ${{ matrix.python }} allow-prereleases: true - name: install dependencies - run: python -m pip install --upgrade pip tox + run: | + python -m pip install --upgrade pip setuptools + python -m pip install tox - name: run env: TOXENV: ${{ matrix.toxenv }} diff --git a/.pylintrc b/.pylintrc index 55a6ac1..2f899e8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,8 +1,8 @@ [MASTER] ignore-patterns=test_.*?py -init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc())+'/tilingsgui')" +init-hook="import os, sys; sys.path.append(os.path.join(os.path.dirname(__file__), 'tilingsgui'))" max-args=10 max-module-lines=1200 -disable=too-few-public-methods,too-many-lines +disable=too-few-public-methods,too-many-lines,too-many-positional-arguments good-names=r,c,x,y,h,w,i,j,k,n,x1,x2,y1,y2,g,b,dx,dy diff --git a/pyproject.toml b/pyproject.toml index d184c0f..25e376c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + [tool.black] target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313'] include = '\.pyi?$' diff --git a/tilingsgui/widgets.py b/tilingsgui/widgets.py index dd04557..dd013a5 100644 --- a/tilingsgui/widgets.py +++ b/tilingsgui/widgets.py @@ -29,7 +29,7 @@ def __init__(self, init_text: str, font_size: int, color: RGBA) -> None: self._document: pyglet.text.document.UnformattedDocument = ( pyglet.text.document.UnformattedDocument(init_text) ) - self._document.set_style(0, 0, dict(font_size=font_size, color=color)) + self._document.set_style(0, 0, {"font_size": font_size, "color": color}) self._layout: pyglet.text.layout.IncrementalTextLayout = ( pyglet.text.layout.IncrementalTextLayout( self._document, 0, 0, multiline=False, batch=self._batch diff --git a/tox.ini b/tox.ini index bacb4a6..d2fb7a9 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ basepython = pypy39: pypy3.9 pypy310: pypy3.10 deps = + setuptools pytest>=8.0.0 pytest-timeout>=2.3.0 commands = pytest From b00ab0ee5a42b482d8891b51ea6ed76510c8c49c Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 14:54:16 +0000 Subject: [PATCH 03/11] Refactor drawing to use pyglet.shapes and improve macOS support Replaces low-level pyglet.graphics drawing calls with pyglet.shapes primitives for lines, circles, and rectangles in graphics and widgets modules. Adds platform-specific handling for PyObjC dependencies and pyglet options for better compatibility on macOS, including PyPy support. Updates setup.py to install PyObjC only on CPython/macOS, and improves documentation for macOS and PyPy users. --- README.rst | 15 +++++++++++- setup.py | 15 +++++++++++- tilingsgui/app.py | 11 +++++++-- tilingsgui/graphics.py | 55 +++++++++++++++++++----------------------- tilingsgui/widgets.py | 46 +++++++++++++++++++++++------------ 5 files changed, 93 insertions(+), 49 deletions(-) diff --git a/README.rst b/README.rst index 736c34f..e33a6d6 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,20 @@ Pyperclip requires clipboard tools that might not come pre-installed. sudo apt-get install xclip -Without them the app still works but pasting won’t. +Without them the app still works but pasting won't. + +Note for macOS +~~~~~~~~~~~~~~ + +PyObjC is automatically installed on macOS systems for proper Cocoa/OpenGL integration with pyglet 2.0+. If you encounter OpenGL surface errors, ensure PyObjC is installed: + +.. code:: sh + + pip install pyobjc-core pyobjc-framework-Cocoa + +This is handled automatically during normal installation. + +**PyPy Users**: PyObjC doesn't support PyPy. TilingsGUI automatically configures pyglet to use shadow context disabled mode for PyPy compatibility on macOS. Known issues ------------ diff --git a/setup.py b/setup.py index e5bb0da..83b15b8 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ #!/usr/bin/env python import os +import sys from setuptools import find_packages, setup @@ -16,6 +17,18 @@ def get_version(): raise ValueError("Version not found in tilingsgui/__init__.py") +def get_install_requires(): + """Get install requirements, including platform-specific dependencies.""" + base_requires = ["pyperclip>=1.9.0", "pyglet>=2.0.0", "tilings>=2.5.0"] + + # Add macOS-specific dependencies for pyglet Cocoa integration + # Only on CPython, not PyPy (PyObjC doesn't support PyPy) + if sys.platform == "darwin" and sys.implementation.name == "cpython": + base_requires.extend(["pyobjc-core", "pyobjc-framework-Cocoa"]) + + return base_requires + + setup( name="tilingsgui", version=get_version(), @@ -31,7 +44,7 @@ def get_version(): }, packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), long_description=read("README.rst"), - install_requires=["pyperclip>=1.9.0", "pyglet>=1.5.15,<2.0", "tilings>=2.5.0"], + install_requires=get_install_requires(), python_requires=">=3.8", include_package_data=True, classifiers=[ diff --git a/tilingsgui/app.py b/tilingsgui/app.py index 6b321ca..a3c2816 100644 --- a/tilingsgui/app.py +++ b/tilingsgui/app.py @@ -5,10 +5,17 @@ # pylint: disable=abstract-method +import sys from typing import ClassVar, Tuple import pyglet +# Configure pyglet for PyPy compatibility on macOS +# PyPy doesn't support PyObjC, so we disable shadow context for high-res displays +if sys.platform == "darwin" and sys.implementation.name == "pypy": + pyglet.options["shadow_window"] = False + +# pylint: disable=wrong-import-position from .files import History, PathManager from .graphics import Color from .menu import RightMenu, TopMenu @@ -90,7 +97,7 @@ def _initial_config(self) -> None: """Configuration done before starting.""" # Center the window within the os. - screen = pyglet.canvas.Display().get_default_screen() + screen = pyglet.display.Display().get_default_screen() # type: ignore self.set_location( (screen.width - self.width) // 2, (screen.height - self.height) // 2 ) @@ -100,7 +107,7 @@ def _initial_config(self) -> None: # Handle clearing the canvas on each draw. pyglet.gl.glClearColor(*TilingGui._CLEAR_COLOR) - self.push_handlers(on_draw=self.clear) + self.push_handlers(on_draw=self.clear) # pylint: disable=unreachable def on_resize(self, width: int, height: int) -> bool: """Event handler for the window resize event. diff --git a/tilingsgui/graphics.py b/tilingsgui/graphics.py index 9b99012..33d15b7 100644 --- a/tilingsgui/graphics.py +++ b/tilingsgui/graphics.py @@ -1,9 +1,9 @@ """Drawable objects""" -from math import cos, pi, sin from typing import ClassVar, List, Tuple import pyglet +import pyglet.shapes from .geometry import Point @@ -32,12 +32,10 @@ def draw_line_segment( y2 (float): End y coordinate. color (Tuple[float, float, float]): RGB valued color. """ - pyglet.graphics.draw( - 2, - pyglet.gl.GL_LINE_STRIP, - (GeoDrawer._VERTEX_MODE, [x1, y1, x2, y2]), - (GeoDrawer._COLOR_MODE, color * 2), - ) + # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes + color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255) + line = pyglet.shapes.Line(x1, y1, x2, y2, color=color_255) + line.draw() @staticmethod def draw_circle(x: float, y: float, r: float, color: C3F, splits: int = 30) -> None: @@ -51,17 +49,11 @@ def draw_circle(x: float, y: float, r: float, color: C3F, splits: int = 30) -> N splits (int, optional): How detailed the polygon emulating a circle should be. Higher values increase detail. """ - vertices = [x, y] - for i in range(splits + 1): - ang = 2 * pi * i / splits - vertices.append(x + cos(ang) * r) - vertices.append(y + sin(ang) * r) - pyglet.graphics.draw( - splits + 2, - pyglet.gl.GL_TRIANGLE_FAN, - (GeoDrawer._VERTEX_MODE, vertices), - (GeoDrawer._COLOR_MODE, color * (splits + 2)), - ) + # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes + # Note: pyglet.shapes.Circle uses segments parameter for detail level + color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255) + circle = pyglet.shapes.Circle(x, y, r, color=color_255, segments=splits) + circle.draw() @staticmethod def draw_point(point: Point, size: float, color: C3F) -> None: @@ -85,12 +77,10 @@ def draw_rectangle(x: float, y: float, w: float, h: float, color: C3F) -> None: h (float): Vertical length. color (Tuple[float, float, float]): Fill color. """ - pyglet.graphics.draw( - 4, - pyglet.gl.GL_TRIANGLE_STRIP, - (GeoDrawer._VERTEX_MODE, [x, y, x, y + h, x + w, y, x + w, y + h]), - (GeoDrawer._COLOR_MODE, color * 4), - ) + # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes + color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255) + rectangle = pyglet.shapes.Rectangle(x, y, w, h, color=color_255) + rectangle.draw() @staticmethod def draw_point_path(pnt_path: List[Point], color: C3F, point_size: float) -> None: @@ -104,13 +94,18 @@ def draw_point_path(pnt_path: List[Point], color: C3F, point_size: float) -> Non n = len(pnt_path) if n > 0: if n > 1: - vertices = [coord for pnt in pnt_path for coord in pnt.coords()] - pyglet.graphics.draw( - n, - pyglet.gl.GL_LINE_STRIP, - (GeoDrawer._VERTEX_MODE, vertices), - (GeoDrawer._COLOR_MODE, color * n), + # Draw line segments between adjacent points + color_255 = ( + int(color[0] * 255), + int(color[1] * 255), + int(color[2] * 255), + 255, ) + for i in range(n - 1): + p1, p2 = pnt_path[i], pnt_path[i + 1] + line = pyglet.shapes.Line(p1.x, p1.y, p2.x, p2.y, color=color_255) + line.draw() + # Draw points for pnt in pnt_path: GeoDrawer.draw_point(pnt, point_size, color) diff --git a/tilingsgui/widgets.py b/tilingsgui/widgets.py index dd013a5..ab53c6d 100644 --- a/tilingsgui/widgets.py +++ b/tilingsgui/widgets.py @@ -32,7 +32,13 @@ def __init__(self, init_text: str, font_size: int, color: RGBA) -> None: self._document.set_style(0, 0, {"font_size": font_size, "color": color}) self._layout: pyglet.text.layout.IncrementalTextLayout = ( pyglet.text.layout.IncrementalTextLayout( - self._document, 0, 0, multiline=False, batch=self._batch + self._document, + x=0, + y=0, + width=100, + height=20, # Will be updated in position() + multiline=False, + batch=self._batch, ) ) self._caret: pyglet.text.caret.Caret = pyglet.text.caret.Caret(self._layout) @@ -47,10 +53,10 @@ def position(self, x: float, y: float, w: float, h: float) -> None: w (float): The horizontal length of the component. h (float): The vertical length of the component. """ - self._layout.x = x + Text._LEFT_PAD - self._layout.y = y - self._layout.width = w - Text._LEFT_PAD - self._layout.height = h + self._layout.x = int(x + Text._LEFT_PAD) + self._layout.y = int(y) + self._layout.width = int(w - Text._LEFT_PAD) + self._layout.height = int(h) def set_focus(self) -> None: """Set focus on the input text. This is needed to write to it.""" @@ -118,12 +124,20 @@ def __init__( box_color (Tuple[float, float, float]): The rgb color of the box. """ super().__init__(init_text, font_size, text_color) - self._vertex_list: pyglet.graphics.vertexdomain.VertexList = self._batch.add( - 4, - pyglet.gl.GL_QUADS, - None, - ("v2f", [0] * 8), - ("c3B", box_color * 4), + # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes + box_color_255 = ( + int(box_color[0] * 255), + int(box_color[1] * 255), + int(box_color[2] * 255), + 255, + ) + self._rectangle = pyglet.shapes.Rectangle( + x=0, + y=0, + width=100, + height=20, # Will be updated in position() + color=box_color_255, + batch=self._batch, ) def position(self, x: float, y: float, w: float, h: float) -> None: @@ -136,8 +150,10 @@ def position(self, x: float, y: float, w: float, h: float) -> None: h (float): The vertical length of the component. """ super().position(x, y, w, h) - for i, vertex in enumerate((x, y, x + w, y, x + w, y + h, x, y + h)): - self._vertex_list.vertices[i] = vertex + self._rectangle.x = x + self._rectangle.y = y + self._rectangle.width = w + self._rectangle.height = h def hit_test(self, x: float, y: float) -> bool: """Is the point (x,y) inside the rectangle that the text box forms. @@ -150,8 +166,8 @@ def hit_test(self, x: float, y: float) -> bool: [type]: True iff inside. """ return ( - self._vertex_list.vertices[0] < x < self._vertex_list.vertices[2] - and self._vertex_list.vertices[1] < y < self._vertex_list.vertices[5] + self._rectangle.x < x < self._rectangle.x + self._rectangle.width + and self._rectangle.y < y < self._rectangle.y + self._rectangle.height ) From 056e1c435ea57079c56dbfb832b8fff03a7e8c40 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 14:58:24 +0000 Subject: [PATCH 04/11] Update type annotations and suppress type warning Changed the return type of TilingGui.on_resize to Literal[True] for more precise typing. Added a type: ignore comment when instantiating TilingGui in main.py to suppress a type checker warning. --- tilingsgui/app.py | 4 ++-- tilingsgui/main.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tilingsgui/app.py b/tilingsgui/app.py index a3c2816..c1abbee 100644 --- a/tilingsgui/app.py +++ b/tilingsgui/app.py @@ -6,7 +6,7 @@ # pylint: disable=abstract-method import sys -from typing import ClassVar, Tuple +from typing import ClassVar, Literal, Tuple import pyglet @@ -109,7 +109,7 @@ def _initial_config(self) -> None: pyglet.gl.glClearColor(*TilingGui._CLEAR_COLOR) self.push_handlers(on_draw=self.clear) # pylint: disable=unreachable - def on_resize(self, width: int, height: int) -> bool: + def on_resize(self, width: int, height: int) -> Literal[True]: """Event handler for the window resize event. Args: diff --git a/tilingsgui/main.py b/tilingsgui/main.py index 119b19d..361c749 100644 --- a/tilingsgui/main.py +++ b/tilingsgui/main.py @@ -16,7 +16,7 @@ def get_args() -> str: def main() -> None: """The application's starting point.""" - app = TilingGui(get_args(), resizable=True) + app = TilingGui(get_args(), resizable=True) # type: ignore app.start() From ffc6ea57c08c16c40265703356bd9e61c260598e Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 15:22:06 +0000 Subject: [PATCH 05/11] Update UI colors and font sizes for improved appearance Increased the top bar height and font sizes in the menu for better readability. Updated text box and background colors in the menu for a lighter appearance. Standardized color usage in tplot by applying scale_to_01 to all color constants. --- tilingsgui/app.py | 2 +- tilingsgui/menu.py | 12 ++++++------ tilingsgui/tplot.py | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/tilingsgui/app.py b/tilingsgui/app.py index c1abbee..e33fb91 100644 --- a/tilingsgui/app.py +++ b/tilingsgui/app.py @@ -32,7 +32,7 @@ class TilingGui(pyglet.window.Window): _INITIAL_WIDTH: ClassVar[int] = 800 _INITIAL_HEIGHT: ClassVar[int] = 650 _RIGHT_BAR_WIDTH: ClassVar[int] = 200 - _TOP_BAR_HEIGHT: ClassVar[int] = 24 + _TOP_BAR_HEIGHT: ClassVar[int] = 40 _CLEAR_COLOR: ClassVar[Tuple[float, float, float, float]] = ( Color.alpha_extend_and_scale_to_01(Color.WHITE) ) diff --git a/tilingsgui/menu.py b/tilingsgui/menu.py index a943e0e..25b6c80 100644 --- a/tilingsgui/menu.py +++ b/tilingsgui/menu.py @@ -20,9 +20,9 @@ class TopMenu(pyglet.event.EventDispatcher, Observer): _PADDING = 1 _INITIAL_MESSAGE = " -- Basis here -- e.g. 1234_1324" - _FONT_SIZE = 12 + _FONT_SIZE = 14 _TEXT_COLOR = Color.alpha_extend(Color.BLACK) - _TEXT_BOX_COLOR = Color.DARK_GRAY + _TEXT_BOX_COLOR = Color.scale_to_01(Color.WHITE) _BACKGROUND_COLOR = Color.BLACK _V_BTN = 118 _CTRL_MODIFIER = 18 @@ -192,11 +192,11 @@ class RightMenu(pyglet.event.EventDispatcher, Observer): """A menu that sits to the right of the tiling plot.""" _PADDING = 1 - _INITIAL_MESSAGE = "12" - _FONT_SIZE = 12 + _INITIAL_MESSAGE = "Req: 12" + _FONT_SIZE = 14 _TEXT_COLOR = Color.alpha_extend(Color.BLACK) - _TEXT_BOX_COLOR = Color.DARK_GRAY - _BACKGROUND_COLOR = Color.BLACK + _TEXT_BOX_COLOR = Color.scale_to_01(Color.WHITE) + _BACKGROUND_COLOR = Color.scale_to_01(Color.LIGHT_GRAY) def __init__( self, diff --git a/tilingsgui/tplot.py b/tilingsgui/tplot.py index fcf1e59..7750e5a 100644 --- a/tilingsgui/tplot.py +++ b/tilingsgui/tplot.py @@ -34,11 +34,19 @@ class TPlot: REQ_NOT_FOUND: ClassVar[Tuple[int, int, int]] = (-1, -1, -1) OBS_NOT_FOUND: ClassVar[Tuple[int, int]] = (-1, -1) - _OBSTRUCTION_COLOR: ClassVar[Tuple[float, float, float]] = Color.RED - _REQUIREMENT_COLOR: ClassVar[Tuple[float, float, float]] = Color.BLUE - _HIGHLIGHT_COLOR: ClassVar[Tuple[float, float, float]] = Color.ORANGE - _EMPTY_COLOR: ClassVar[Tuple[float, float, float]] = Color.GRAY - _SHADED_CELL_COLOR: ClassVar[Tuple[float, float, float]] = Color.GRAY + _OBSTRUCTION_COLOR: ClassVar[Tuple[float, float, float]] = Color.scale_to_01( + Color.RED + ) + _REQUIREMENT_COLOR: ClassVar[Tuple[float, float, float]] = Color.scale_to_01( + Color.BLUE + ) + _HIGHLIGHT_COLOR: ClassVar[Tuple[float, float, float]] = Color.scale_to_01( + Color.ORANGE + ) + _EMPTY_COLOR: ClassVar[Tuple[float, float, float]] = Color.scale_to_01(Color.GRAY) + _SHADED_CELL_COLOR: ClassVar[Tuple[float, float, float]] = Color.scale_to_01( + Color.GRAY + ) _FUZZYNESS = 0.25 # Should be in [0,0.5) _CLICK_PRECISION_SQUARED: int = 100 _POINT_SIZE = 5 From b811d8a6688fefc1453de59fb07f920bf328d3fe Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 17:29:27 +0000 Subject: [PATCH 06/11] Increase UI dimensions and improve widget scaling Updated initial window and UI bar sizes for a larger interface. Increased font sizes in menu components for better readability. Improved text and button image positioning and scaling in widgets for more consistent appearance. Fixed a typo in ButtonGrid's rect assignment and adjusted padding. --- tilingsgui/app.py | 8 ++++---- tilingsgui/menu.py | 4 ++-- tilingsgui/widgets.py | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tilingsgui/app.py b/tilingsgui/app.py index e33fb91..a2ee56e 100644 --- a/tilingsgui/app.py +++ b/tilingsgui/app.py @@ -29,10 +29,10 @@ class TilingGui(pyglet.window.Window): _TITLE: ClassVar[str] = "Tilings GUI" _MIN_WIDTH: ClassVar[int] = 500 _MIN_HEIGHT: ClassVar[int] = 400 - _INITIAL_WIDTH: ClassVar[int] = 800 - _INITIAL_HEIGHT: ClassVar[int] = 650 - _RIGHT_BAR_WIDTH: ClassVar[int] = 200 - _TOP_BAR_HEIGHT: ClassVar[int] = 40 + _INITIAL_WIDTH: ClassVar[int] = 1600 + _INITIAL_HEIGHT: ClassVar[int] = 1200 + _RIGHT_BAR_WIDTH: ClassVar[int] = 400 + _TOP_BAR_HEIGHT: ClassVar[int] = 50 _CLEAR_COLOR: ClassVar[Tuple[float, float, float, float]] = ( Color.alpha_extend_and_scale_to_01(Color.WHITE) ) diff --git a/tilingsgui/menu.py b/tilingsgui/menu.py index 25b6c80..0d73723 100644 --- a/tilingsgui/menu.py +++ b/tilingsgui/menu.py @@ -20,7 +20,7 @@ class TopMenu(pyglet.event.EventDispatcher, Observer): _PADDING = 1 _INITIAL_MESSAGE = " -- Basis here -- e.g. 1234_1324" - _FONT_SIZE = 14 + _FONT_SIZE = 20 _TEXT_COLOR = Color.alpha_extend(Color.BLACK) _TEXT_BOX_COLOR = Color.scale_to_01(Color.WHITE) _BACKGROUND_COLOR = Color.BLACK @@ -193,7 +193,7 @@ class RightMenu(pyglet.event.EventDispatcher, Observer): _PADDING = 1 _INITIAL_MESSAGE = "Req: 12" - _FONT_SIZE = 14 + _FONT_SIZE = 20 _TEXT_COLOR = Color.alpha_extend(Color.BLACK) _TEXT_BOX_COLOR = Color.scale_to_01(Color.WHITE) _BACKGROUND_COLOR = Color.scale_to_01(Color.LIGHT_GRAY) diff --git a/tilingsgui/widgets.py b/tilingsgui/widgets.py index ab53c6d..d66db9d 100644 --- a/tilingsgui/widgets.py +++ b/tilingsgui/widgets.py @@ -54,7 +54,7 @@ def position(self, x: float, y: float, w: float, h: float) -> None: h (float): The vertical length of the component. """ self._layout.x = int(x + Text._LEFT_PAD) - self._layout.y = int(y) + self._layout.y = int(y - 15) self._layout.width = int(w - Text._LEFT_PAD) self._layout.height = int(h) @@ -229,9 +229,15 @@ def position(self, x: float, y: float, w: float, h: float) -> None: h (float): The vertical length of the button. """ self._x, self._y, self._w, self._h = x, y, w, h - dim = min(w, h) - if dim > 0: - self._sprite.scale *= dim / self._sprite.width + if w > 0 and h > 0: + # Scale to fit within the button bounds while maintaining aspect ratio + scale_x = w / self._sprite.image.width + scale_y = h / self._sprite.image.height + # Use the smaller scale to ensure image fits within bounds + scale = ( + min(scale_x, scale_y) * 0.95 + ) # 0.95 for slightly smaller, well-proportioned images + self._sprite.scale = scale self._sprite.x = x + self._w / 2 - self._sprite.width / 2 self._sprite.y = y + self._h / 2 - self._sprite.height / 2 @@ -388,7 +394,7 @@ def __contains__(self, key: Tuple[int, int]) -> bool: class ButtonGrid: """A positional object to place and group buttons together.""" - _PADDING: ClassVar[int] = 1 + _PADDING: ClassVar[int] = 2 def __init__(self, r: int, c: int) -> None: """Create a button grid with r rows and c columns. @@ -428,7 +434,7 @@ def position(self, x: float, y: float, w: float, h: float) -> None: w (float): The horizontal length of the grid. h (float): The vertical length of the grid. """ - self.rect.x, self.rect.y, self.rect.h, self.rect.h = x, y, w, h + self.rect.x, self.rect.y, self.rect.w, self.rect.h = x, y, w, h self.button_w, self.button_h = w / len(self.buttons[0]), h / len(self.buttons) self._position_btns() From 96e104a01d1f6c96b7e08b515d6ac4633d995ffd Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 17:40:09 +0000 Subject: [PATCH 07/11] Exit on PyPy/macOS due to pyglet incompatibility Replace disabling of pyglet's shadow window with an explicit warning and exit when running on PyPy with macOS, as this configuration is not supported. This prevents users from encountering subtle runtime issues. --- tilingsgui/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tilingsgui/app.py b/tilingsgui/app.py index a2ee56e..1151f53 100644 --- a/tilingsgui/app.py +++ b/tilingsgui/app.py @@ -11,9 +11,10 @@ import pyglet # Configure pyglet for PyPy compatibility on macOS -# PyPy doesn't support PyObjC, so we disable shadow context for high-res displays if sys.platform == "darwin" and sys.implementation.name == "pypy": - pyglet.options["shadow_window"] = False + print("Warning: PyPy on macOS has known compatibility issues with pyglet 2.0.") + print("For best results, use CPython instead of PyPy on macOS.") + sys.exit(1) # pylint: disable=wrong-import-position from .files import History, PathManager From 5c09eb734e7d7a74a3c179146654995ef4d05329 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 17:44:53 +0000 Subject: [PATCH 08/11] Cast width and height to float for scaling Explicitly cast button width and height to float when calculating scale factors to ensure correct aspect ratio scaling and avoid integer division issues. --- tilingsgui/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tilingsgui/widgets.py b/tilingsgui/widgets.py index d66db9d..7cb4e98 100644 --- a/tilingsgui/widgets.py +++ b/tilingsgui/widgets.py @@ -231,8 +231,8 @@ def position(self, x: float, y: float, w: float, h: float) -> None: self._x, self._y, self._w, self._h = x, y, w, h if w > 0 and h > 0: # Scale to fit within the button bounds while maintaining aspect ratio - scale_x = w / self._sprite.image.width - scale_y = h / self._sprite.image.height + scale_x = float(w) / float(self._sprite.image.width) + scale_y = float(h) / float(self._sprite.image.height) # Use the smaller scale to ensure image fits within bounds scale = ( min(scale_x, scale_y) * 0.95 From 6deb36052d4d2685110163d02d089ee8a9feb433 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 28 Sep 2025 17:47:09 +0000 Subject: [PATCH 09/11] Improve sprite scaling for Button widget Updated the Button widget to handle both AbstractImage and Animation types when scaling sprites. The code now checks for width and height attributes on the image, falling back to sprite dimensions if necessary, ensuring correct scaling for different image types. --- tilingsgui/widgets.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tilingsgui/widgets.py b/tilingsgui/widgets.py index 7cb4e98..4386823 100644 --- a/tilingsgui/widgets.py +++ b/tilingsgui/widgets.py @@ -231,8 +231,18 @@ def position(self, x: float, y: float, w: float, h: float) -> None: self._x, self._y, self._w, self._h = x, y, w, h if w > 0 and h > 0: # Scale to fit within the button bounds while maintaining aspect ratio - scale_x = float(w) / float(self._sprite.image.width) - scale_y = float(h) / float(self._sprite.image.height) + # Handle both AbstractImage and Animation types + image = self._sprite.image + if hasattr(image, "width") and hasattr(image, "height"): + img_width = float(image.width) + img_height = float(image.height) + else: + # Fallback for Animation or other types - use sprite dimensions + img_width = float(self._sprite.width) + img_height = float(self._sprite.height) + + scale_x = float(w) / img_width + scale_y = float(h) / img_height # Use the smaller scale to ensure image fits within bounds scale = ( min(scale_x, scale_y) * 0.95 From 288d211dd44718f9fc55b1ea8582a1f0c6836c46 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 5 Oct 2025 15:41:12 +0000 Subject: [PATCH 10/11] Refactor color conversion logic and cleanup dependencies Moved RGB-to-RGBA color conversion into a new Color.scale_to_255 method and updated all usages in graphics.py and widgets.py to use this utility. Removed platform-specific dependencies from setup.py for a simpler install_requires. --- setup.py | 12 ++--------- tilingsgui/graphics.py | 46 +++++++++++++++++++++--------------------- tilingsgui/widgets.py | 8 +------- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/setup.py b/setup.py index 83b15b8..4d06e32 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ #!/usr/bin/env python import os -import sys from setuptools import find_packages, setup @@ -18,15 +17,8 @@ def get_version(): def get_install_requires(): - """Get install requirements, including platform-specific dependencies.""" - base_requires = ["pyperclip>=1.9.0", "pyglet>=2.0.0", "tilings>=2.5.0"] - - # Add macOS-specific dependencies for pyglet Cocoa integration - # Only on CPython, not PyPy (PyObjC doesn't support PyPy) - if sys.platform == "darwin" and sys.implementation.name == "cpython": - base_requires.extend(["pyobjc-core", "pyobjc-framework-Cocoa"]) - - return base_requires + """Get install requirements.""" + return ["pyperclip>=1.9.0", "pyglet>=2.0.0", "tilings>=2.5.0"] setup( diff --git a/tilingsgui/graphics.py b/tilingsgui/graphics.py index 33d15b7..c1d23a5 100644 --- a/tilingsgui/graphics.py +++ b/tilingsgui/graphics.py @@ -16,9 +16,6 @@ class GeoDrawer: """A static class container of drawing methods.""" - _VERTEX_MODE: ClassVar[str] = "v2f" - _COLOR_MODE: ClassVar[str] = "c3B" - @staticmethod def draw_line_segment( x1: float, y1: float, x2: float, y2: float, color: C3F @@ -32,8 +29,7 @@ def draw_line_segment( y2 (float): End y coordinate. color (Tuple[float, float, float]): RGB valued color. """ - # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes - color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255) + color_255 = Color.scale_to_255(color) line = pyglet.shapes.Line(x1, y1, x2, y2, color=color_255) line.draw() @@ -49,9 +45,7 @@ def draw_circle(x: float, y: float, r: float, color: C3F, splits: int = 30) -> N splits (int, optional): How detailed the polygon emulating a circle should be. Higher values increase detail. """ - # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes - # Note: pyglet.shapes.Circle uses segments parameter for detail level - color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255) + color_255 = Color.scale_to_255(color) circle = pyglet.shapes.Circle(x, y, r, color=color_255, segments=splits) circle.draw() @@ -77,8 +71,7 @@ def draw_rectangle(x: float, y: float, w: float, h: float, color: C3F) -> None: h (float): Vertical length. color (Tuple[float, float, float]): Fill color. """ - # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes - color_255 = (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255) + color_255 = Color.scale_to_255(color) rectangle = pyglet.shapes.Rectangle(x, y, w, h, color=color_255) rectangle.draw() @@ -95,12 +88,7 @@ def draw_point_path(pnt_path: List[Point], color: C3F, point_size: float) -> Non if n > 0: if n > 1: # Draw line segments between adjacent points - color_255 = ( - int(color[0] * 255), - int(color[1] * 255), - int(color[2] * 255), - 255, - ) + color_255 = Color.scale_to_255(color) for i in range(n - 1): p1, p2 = pnt_path[i], pnt_path[i + 1] line = pyglet.shapes.Line(p1.x, p1.y, p2.x, p2.y, color=color_255) @@ -257,13 +245,13 @@ class Color: @staticmethod def scale_to_01(color: C3I) -> C3F: - """Scale color values from 0-255 to 0-1. Color can include alpha value. + """Scale color values from 0-255 to 0-1. Args: - color (Tuple[int, int, int]): A tuple of length 3. + color (Tuple[int, int, int]): RGB color in 0-255 range. Returns: - Tuple[float, float, float]: A scaled tuple. + Tuple[float, float, float]: RGB color in 0-1 range. """ r, g, b = color return r / 255, g / 255, b / 255 @@ -284,15 +272,27 @@ def alpha_extend(color: C3I, alpha: int = 255) -> C4I: @staticmethod def alpha_extend_and_scale_to_01(color: C3I, alpha: int = 255) -> C4F: """Performs both alpha extension and 0-1 scaling of a color. - Use this method if you intent to do both to guarantee they + Use this method if you intend to do both to guarantee they are done in correct order. Args: - color (Tuple[int, int, int]): A rgb color value. - alpha (int, optional): The alpha value, 0-255. Defaults to 255. + color (Tuple[int, int, int]): RGB color in 0-255 range. + alpha (int, optional): Alpha value in 0-255 range. Defaults to 255. Returns: - Tuple[int, int, int, int]: A rgba color value, 0-1. + Tuple[float, float, float, float]: RGBA color in 0-1 range. """ r, g, b = color return r / 255, g / 255, b / 255, alpha / 255 + + @staticmethod + def scale_to_255(color: C3F) -> C4I: + """Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes. + + Args: + color (Tuple[float, float, float]): RGB color in 0-1 range. + + Returns: + Tuple[int, int, int, int]: RGBA color in 0-255 range. + """ + return (int(color[0] * 255), int(color[1] * 255), int(color[2] * 255), 255) diff --git a/tilingsgui/widgets.py b/tilingsgui/widgets.py index 4386823..72945cd 100644 --- a/tilingsgui/widgets.py +++ b/tilingsgui/widgets.py @@ -124,13 +124,7 @@ def __init__( box_color (Tuple[float, float, float]): The rgb color of the box. """ super().__init__(init_text, font_size, text_color) - # Convert RGB (0-1) to RGBA (0-255) for pyglet.shapes - box_color_255 = ( - int(box_color[0] * 255), - int(box_color[1] * 255), - int(box_color[2] * 255), - 255, - ) + box_color_255 = Color.scale_to_255(box_color) self._rectangle = pyglet.shapes.Rectangle( x=0, y=0, From 60308ad6d5860e0f68efc43900f4c49841517e2f Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Wed, 15 Oct 2025 20:42:04 +0000 Subject: [PATCH 11/11] Clarify PyPy incompatibility with macOS GUI frameworks Expanded comments to document the fundamental incompatibility between PyPy's ctypes and macOS GUI frameworks, affecting all major Python GUI libraries. Updated runtime check to print a more explicit error and exit if PyPy is detected on macOS, advising users to use CPython or try PyPy on other platforms. --- tilingsgui/app.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/tilingsgui/app.py b/tilingsgui/app.py index 1151f53..2702dd1 100644 --- a/tilingsgui/app.py +++ b/tilingsgui/app.py @@ -10,10 +10,41 @@ import pyglet -# Configure pyglet for PyPy compatibility on macOS +# PyPy compatibility on macOS +# +# PyPy is not supported on macOS due to fundamental incompatibilities between PyPy's +# ctypes implementation and macOS GUI frameworks. This issue affects ALL major Python +# GUI libraries, not just pyglet. +# +# Investigation conducted (Oct 2025) found the following: +# +# 1. pyglet 2.0: +# - Fails with IndexError/AttributeError in PyPy's ctypes when interfacing with +# Cocoa/Objective-C bridge +# - pyglet.options like shadow_window=False and osx_alt_loop=True do not help +# +# 2. Tkinter: +# - Imports successfully but hangs when creating windows +# - PyPy's bundled Tk requires manual mainloop() calls, breaking event-driven design +# - Tested with PyPy 7.3.20 (Python 3.11.13) - same issues +# +# 3. PySide6 (Qt): +# - Officially supports PyPy 3.8+ but no pre-built PyPy wheels available for macOS +# - Would require building from source +# +# 4. wxPython: +# - Community reports indicate it does not work with PyPy +# +# Root cause: PyPy's ctypes has subtle differences from CPython in callback and object +# reference handling, which breaks all macOS GUI frameworks that use ctypes to interface +# with Cocoa/Objective-C. +# +# Note: PyPy may work on Linux (X11) or Windows (Win32), as the issue is specific to +# macOS's Cocoa backend. if sys.platform == "darwin" and sys.implementation.name == "pypy": - print("Warning: PyPy on macOS has known compatibility issues with pyglet 2.0.") - print("For best results, use CPython instead of PyPy on macOS.") + print("Error: PyPy is not supported on macOS.") + print("Reason: PyPy's ctypes is incompatible with macOS GUI frameworks (Cocoa).") + print("Please use CPython on macOS, or try PyPy on Linux/Windows.") sys.exit(1) # pylint: disable=wrong-import-position