From ba63bbab314da347f353d7f4c1b4064abe1b5f18 Mon Sep 17 00:00:00 2001 From: Michell Stuttgart Date: Sat, 9 Nov 2024 16:26:25 -0300 Subject: [PATCH] Add new project template --- .github/workflows/build.yml | 212 ++++++ .gitignore | 528 ++++++++++++++ Justfile | 276 +++++++ LICENSE | 674 ++++++++++++++++++ README.MD | 85 +++ build-aux/generate-lupdate-project-file.py | 101 +++ build-aux/generate-qt-creator-project-file.py | 118 +++ build-aux/linux/Justfile | 12 + build-aux/linux/flatpak-pypi-checker.py | 120 ++++ build-aux/linux/krb5.conf | 9 + ...org.github.trin94.pyside6.template.desktop | 8 + ...ithub.trin94.pyside6.template.metainfo.xml | 28 + .../org.github.trin94.pyside6.template.svg | 3 + .../org.github.trin94.pyside6.template.yml | 110 +++ build-aux/windows/icon.ico | Bin 0 -> 6819 bytes data/app-icon.svg | 67 ++ data/icons/close_black_24dp.svg | 1 + data/icons/close_fullscreen_black_24dp.svg | 1 + data/icons/minimize_black_24dp.svg | 1 + data/icons/open_in_full_black_24dp.svg | 1 + data/qtquickcontrols2.conf | 7 + docs/dev-setup-linux.md | 14 + docs/dev-setup-windows.md | 18 + docs/internationalization.md | 16 + docs/picture.png | Bin 0 -> 42451 bytes i18n/de_DE.ts | 88 +++ i18n/he_IL.ts | 88 +++ main.py | 11 + myapp/__init__.py | 14 + myapp/application.py | 84 +++ myapp/framelesswindow/__init__.py | 14 + myapp/framelesswindow/linux/__init__.py | 16 + myapp/framelesswindow/linux/event.py | 69 ++ myapp/framelesswindow/win/__init__.py | 22 + myapp/framelesswindow/win/c_structures.py | 172 +++++ myapp/framelesswindow/win/effect.py | 86 +++ myapp/framelesswindow/win/event.py | 131 ++++ myapp/pyobjects/__init__.py | 17 + myapp/pyobjects/example_singleton.py | 32 + myapp/startup.py | 72 ++ qml/app/MyAppMainPage.qml | 97 +++ qml/app/qmldir | 2 + qml/header/MyAppHeader.qml | 60 ++ qml/header/MyAppHeaderContent.qml | 135 ++++ qml/header/MyAppHelpMenu.qml | 53 ++ qml/header/MyAppMenu1.qml | 69 ++ qml/header/MyAppMenu2.qml | 53 ++ qml/header/MyAppOptionsMenu.qml | 85 +++ qml/header/qmldir | 2 + qml/main.qml | 83 +++ qml/models/MyAppLanguageModel.qml | 40 ++ qml/models/qmldir | 2 + qml/models/tst_MyAppLanguageModel.qml | 65 ++ qml/shared/MyAppAutoWidthMenu.qml | 52 ++ qml/shared/qmldir | 2 + requirements.txt | 3 + test/__init__.py | 23 + test/services/__init__.py | 13 + test/services/test_resource_availability.py | 33 + 59 files changed, 4198 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 Justfile create mode 100644 LICENSE create mode 100644 README.MD create mode 100755 build-aux/generate-lupdate-project-file.py create mode 100644 build-aux/generate-qt-creator-project-file.py create mode 100755 build-aux/linux/Justfile create mode 100644 build-aux/linux/flatpak-pypi-checker.py create mode 100644 build-aux/linux/krb5.conf create mode 100644 build-aux/linux/org.github.trin94.pyside6.template.desktop create mode 100644 build-aux/linux/org.github.trin94.pyside6.template.metainfo.xml create mode 100644 build-aux/linux/org.github.trin94.pyside6.template.svg create mode 100644 build-aux/linux/org.github.trin94.pyside6.template.yml create mode 100644 build-aux/windows/icon.ico create mode 100644 data/app-icon.svg create mode 100644 data/icons/close_black_24dp.svg create mode 100644 data/icons/close_fullscreen_black_24dp.svg create mode 100644 data/icons/minimize_black_24dp.svg create mode 100644 data/icons/open_in_full_black_24dp.svg create mode 100644 data/qtquickcontrols2.conf create mode 100644 docs/dev-setup-linux.md create mode 100644 docs/dev-setup-windows.md create mode 100644 docs/internationalization.md create mode 100644 docs/picture.png create mode 100644 i18n/de_DE.ts create mode 100644 i18n/he_IL.ts create mode 100755 main.py create mode 100644 myapp/__init__.py create mode 100644 myapp/application.py create mode 100644 myapp/framelesswindow/__init__.py create mode 100644 myapp/framelesswindow/linux/__init__.py create mode 100644 myapp/framelesswindow/linux/event.py create mode 100644 myapp/framelesswindow/win/__init__.py create mode 100644 myapp/framelesswindow/win/c_structures.py create mode 100644 myapp/framelesswindow/win/effect.py create mode 100644 myapp/framelesswindow/win/event.py create mode 100644 myapp/pyobjects/__init__.py create mode 100644 myapp/pyobjects/example_singleton.py create mode 100644 myapp/startup.py create mode 100644 qml/app/MyAppMainPage.qml create mode 100644 qml/app/qmldir create mode 100644 qml/header/MyAppHeader.qml create mode 100644 qml/header/MyAppHeaderContent.qml create mode 100644 qml/header/MyAppHelpMenu.qml create mode 100644 qml/header/MyAppMenu1.qml create mode 100644 qml/header/MyAppMenu2.qml create mode 100644 qml/header/MyAppOptionsMenu.qml create mode 100644 qml/header/qmldir create mode 100644 qml/main.qml create mode 100644 qml/models/MyAppLanguageModel.qml create mode 100644 qml/models/qmldir create mode 100644 qml/models/tst_MyAppLanguageModel.qml create mode 100644 qml/shared/MyAppAutoWidthMenu.qml create mode 100644 qml/shared/qmldir create mode 100644 requirements.txt create mode 100644 test/__init__.py create mode 100644 test/services/__init__.py create mode 100644 test/services/test_resource_availability.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..3fc1c33f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,212 @@ +name: 'Build' + +on: + push: + branches: [ '**' ] + +defaults: + run: + shell: bash + +jobs: + build_python_linux: + runs-on: ubuntu-latest + name: 'Python (Linux)' + strategy: + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + outputs: + artifact_name: ${{ steps.step_artifact_name.outputs.artifact_name }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: 'Prepare Artifact Name' + id: step_artifact_name + run: | + CURRENT_COMMIT="$(git rev-parse HEAD)" + CURRENT_COMMIT="${CURRENT_COMMIT:0:8}" + echo "git commit: $CURRENT_COMMIT" + + ARTIFACT_NAME="MyApp-$CURRENT_COMMIT" + echo "artifact name: $ARTIFACT_NAME" + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + - name: 'Install Python' + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: 'Install pip' + run: | + python -m pip install --upgrade pip +# - name: 'Update Packages' +# run: | +# sudo apt update -y && sudo apt upgrade -y + - name: 'Install Dependencies' + run: | + sudo apt install -y patchelf libopengl0 libegl-dev + - name: 'Install just' + uses: taiki-e/install-action@just + - name: 'Create Virtual Environment' + run: | + python -m venv venv + source venv/bin/activate + python -m pip install --upgrade pip + python -m pip install wheel + python -m pip install -r requirements.txt + - name: 'Remove Qml Test Files' + run: | + find . -type f -name 'tst_*.qml' -delete + - name: 'Run Python Tests' + run: | + source venv/bin/activate + just test-python + just clean + just build + - name: 'Upload Build Artifact' + uses: actions/upload-artifact@v4 + if: matrix.python-version == '3.12' + with: + path: build/release + name: release-build-artifact + + build_python_windows: + runs-on: windows-latest + name: 'Python (Windows)' + strategy: + matrix: + python-version: [ '3.9', '3.10', '3.11', '3.12' ] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: 'Prepare Artifact Name' + id: step_artifact_name + run: | + CURRENT_COMMIT="$(git rev-parse HEAD)" + CURRENT_COMMIT="${CURRENT_COMMIT:0:8}" + echo "git commit: $CURRENT_COMMIT" + + ARTIFACT_NAME="MyApp-$CURRENT_COMMIT" + echo "artifact name: $ARTIFACT_NAME" + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + - name: 'Install Python' + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: 'Install pip' + run: | + python -m pip install --upgrade pip + - name: 'Install just' + uses: taiki-e/install-action@just + - name: 'Create Virtual Environment' + run: | + python -m venv venv + source venv/Scripts/activate + python -m pip install --upgrade pip + python -m pip install wheel + python -m pip install -r requirements.txt + - name: 'Remove Qml Test Files' + run: | + find . -type f -name 'tst_*.qml' -delete + - name: 'Run Python Tests' + run: | + source venv/Scripts/activate + just test-python + just clean + just build + + test_qml: + runs-on: ubuntu-latest + name: 'Qml' + steps: + - uses: actions/checkout@v4 + - name: 'Install Qt 6.7.2' + uses: jurplel/install-qt-action@v4 + with: + version: '6.7.2' + - name: 'Install just' + uses: taiki-e/install-action@just + - name: 'Run Qml Tests' + run: | + just test-qml + + distributable-windows: + runs-on: windows-latest + name: 'Build Windows' + needs: + - build_python_linux + - build_python_windows + - test_qml + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Download Build Artifact' + uses: actions/download-artifact@v4 + with: + path: build/release + name: release-build-artifact + - name: 'Install Python 3.12' + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: 'Setup Build Environment' + run: | + python -m venv venv + source venv/Scripts/activate + python -m pip install --upgrade pip + python -m pip install wheel + python -m pip install -r requirements.txt + python -m pip install pyinstaller + - name: 'Build Bundle' + run: | + source venv/Scripts/activate + pyinstaller \ + --name MpApp \ + --workpath build-windows \ + --icon=build-aux/windows/icon.ico \ + --collect-binaries PySide6 \ + --add-data "LICENSE;." \ + --noconsole \ + build/release/main.py + - name: 'Remove Redundant Binaries' + run: | + find dist/MpApp -type f -name 'Qt6WebEngineCore.dll' -delete + find dist/MpApp -type f -name 'opengl32sw.dll' -delete + - name: 'Compress Artifact' + shell: pwsh + run: Compress-Archive -Path "dist\MpApp\*" -DestinationPath "${{ needs.build_python_linux.outputs.artifact_name }}.zip" + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: "${{ needs.build_python_linux.outputs.artifact_name }}-win-x86_64" + path: "${{ needs.build_python_linux.outputs.artifact_name }}.zip" + + distributable-linux: + runs-on: ubuntu-latest + name: 'Build Linux (Flatpak)' + container: + image: bilelmoussaoui/flatpak-github-actions:freedesktop-22.08 + options: --privileged + needs: + - build_python_linux + - build_python_windows + - test_qml + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Download Build Artifact' + uses: actions/download-artifact@v4 + with: + path: build/release + name: release-build-artifact + - name: 'Prepare Flatpak Build' + run: | + mv build-aux/linux/org.github.trin94.pyside6.template.yml org.github.trin94.pyside6.template.yml + - name: 'Build Flatpak' + uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + manifest-path: org.github.trin94.pyside6.template.yml + bundle: ${{ needs.build_python_linux.outputs.artifact_name }}-linux.flatpak + branch: main + cache: false + cache-key: flatpak-builder-${{ github.sha }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3d44d59b --- /dev/null +++ b/.gitignore @@ -0,0 +1,528 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,qt,qtcreator,vscode,sublimetext,qml,linux,windows,macos,vim,emacs +# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,qt,qtcreator,vscode,sublimetext,qml,linux,windows,macos,vim,emacs + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive +ltximg/** + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ +.idea + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +# .env +.env/ +.venv/ +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# operating system-related files +# file properties cache/storage on macOS +*.DS_Store +# thumbnail cache on Windows +Thumbs.db + +# profiling data +.prof + + +### QML ### +# Cached binary representations of QML and JS files +*.qmlc +*.jsc + +### Qt ### +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so.* +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +Makefile* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +### QtCreator ### +# gitignore for Qt Creator like IDE for pure C/C++ project without Qt +# +# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html + + + +# Qt Creator autogenerated files + + +# A listing of all the files included in the project +*.files + +# Include directories +*.includes + +# Project configuration settings like predefined Macros +*.config + +# Qt Creator settings +*.creator + +# User project settings +*.creator.user* + +# Qt Creator backups + +# Flags for Clang Code Model +*.cxxflags +*.cflags + + +### SublimeText ### +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### vscode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +### Windows ### +# Windows thumbnail cache files +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,qt,qtcreator,vscode,sublimetext,qml,linux,windows,macos,vim,emacs + +.flatpak-builder +build-dir +myapp/generated_resources.py +test/generated_resources.py diff --git a/Justfile b/Justfile new file mode 100644 index 00000000..a839a189 --- /dev/null +++ b/Justfile @@ -0,0 +1,276 @@ +# Copyright 2024 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +PYTHON_DIR := invocation_directory() + '/' + if os_family() == 'windows' { '.venv/Scripts' } else { '.venv/bin' } +PYTHON := PYTHON_DIR + if os_family() == 'windows' { '/python.exe' } else { '/python3' } + +# + +TOOL_CLI_LUPDATE := PYTHON_DIR + '/pyside6-lupdate' +TOOL_CLI_LRELEASE := PYTHON_DIR + '/pyside6-lrelease' +TOOL_CLI_RCC := PYTHON_DIR + '/pyside6-rcc' +TOOL_CLI_QML_TESTRUNNER := 'qmltestrunner' + +# + +export QT_QPA_PLATFORM := 'offscreen' +export QT_QUICK_CONTROLS_STYLE := 'Material' +export QT_QUICK_CONTROLS_MATERIAL_VARIANT := 'dense' + +##### ##### +##### Names ##### +##### ##### + +NAME_APPLICATION := 'myapp' +NAME_DIRECTORY_BUILD := 'build' +NAME_DIRECTORY_BUILD_HELPERS := 'build-aux' +NAME_DIRECTORY_DATA := 'data' +NAME_DIRECTORY_I18N := 'i18n' +NAME_DIRECTORY_PY_SOURCES := 'myapp' +NAME_DIRECTORY_PY_TESTS := 'test' +NAME_DIRECTORY_QML_SOURCES := 'qml' +NAME_DIRECTORY_QML_TESTS := 'qml' +NAME_FILE_MAIN_ENTRY := 'main.py' +NAME_FILE_GENERATED_RESOURCES := 'generated_resources.py' + +##### ##### +##### Existing Directories ##### +##### ##### + +DIRECTORY_ROOT := invocation_directory() +DIRECTORY_BUILD_HELPERS := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_BUILD_HELPERS +DIRECTORY_DATA := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_DATA +DIRECTORY_I18N := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_I18N +DIRECTORY_PY_SOURCES := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_PY_SOURCES +DIRECTORY_PY_TESTS := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_PY_TESTS +DIRECTORY_QML_SOURCES := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_QML_SOURCES +DIRECTORY_QML_TESTS := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_QML_TESTS + +##### ##### +##### Existing Files ##### +##### ##### + +FILE_APP_ENTRY := DIRECTORY_ROOT + '/' + NAME_FILE_MAIN_ENTRY + +##### ##### +##### Generated Directories ##### +##### ##### + +DIRECTORY_BUILD := DIRECTORY_ROOT + '/' + NAME_DIRECTORY_BUILD +DIRECTORY_BUILD_QRC_QML := DIRECTORY_BUILD + '/qrc-' + NAME_DIRECTORY_QML_SOURCES +DIRECTORY_BUILD_QRC_DATA := DIRECTORY_BUILD + '/qrc-' + NAME_DIRECTORY_DATA +DIRECTORY_BUILD_QRC_I18N := DIRECTORY_BUILD + '/qrc-' + NAME_DIRECTORY_I18N +DIRECTORY_BUILD_TRANSLATIONS := DIRECTORY_BUILD + '/translations' +DIRECTORY_BUILD_RESOURCES := DIRECTORY_BUILD + '/resources' +DIRECTORY_BUILD_RELEASE := DIRECTORY_BUILD + '/release' +DIRECTORY_BUILD_PY := DIRECTORY_BUILD_RELEASE + '/' + NAME_DIRECTORY_PY_SOURCES + +##### ##### +##### Generated Files ##### +##### ##### + +FILE_BUILD_QRC_QML := DIRECTORY_BUILD_QRC_QML + '/' + NAME_DIRECTORY_QML_SOURCES + '.qrc' +FILE_BUILD_QRC_DATA := DIRECTORY_BUILD_QRC_DATA + '/' + NAME_DIRECTORY_DATA + '.qrc' +FILE_BUILD_QRC_I18N := DIRECTORY_BUILD_QRC_I18N + '/' + NAME_DIRECTORY_I18N + '.qrc' +FILE_BUILD_QRC_I18N_JSON := DIRECTORY_BUILD_QRC_I18N + '/' + NAME_APPLICATION + '.json' +FILE_BUILD_TRANSLATIONS_JSON := DIRECTORY_BUILD_TRANSLATIONS + '/' + NAME_APPLICATION + '.json' +FILE_BUILD_RESOURCES := DIRECTORY_BUILD_RESOURCES + '/' + NAME_FILE_GENERATED_RESOURCES +FILE_PY_SOURCES_RESOURCES := DIRECTORY_PY_SOURCES + '/' + NAME_FILE_GENERATED_RESOURCES +FILE_PY_TEST_RESOURCES := DIRECTORY_PY_TESTS + '/' + NAME_FILE_GENERATED_RESOURCES + +_default: + @just --list + +# Build full project into build/release +[group('build')] +build: _check-pyside-setup _clean-build _clean-develop _compile-resources + @rm -rf \ + {{ DIRECTORY_BUILD_PY }} + @mkdir -p \ + {{ DIRECTORY_BUILD_PY }} + @cp -r \ + {{ DIRECTORY_PY_SOURCES }}/. \ + {{ DIRECTORY_BUILD_PY }} + @cp \ + {{ FILE_BUILD_RESOURCES }} \ + {{ DIRECTORY_BUILD_PY }} + @cp \ + {{ FILE_APP_ENTRY }} \ + {{ DIRECTORY_BUILD_RELEASE }} + @echo ''; \ + echo 'Please find the finished project in {{ DIRECTORY_BUILD_RELEASE }}' + +# Build and compile resources into source directory +[group('build')] +build-develop: _check-pyside-setup _clean-develop _compile-resources + @# Generates resources and copies them into the source directory + @# This allows to develop/debug the project normally + + @cp \ + {{ FILE_BUILD_RESOURCES }} {{ DIRECTORY_PY_SOURCES }} + +# Remove ALL generated files +[group('build')] +clean: _clean-build _clean-develop _clean-test + +# Add new language +[group('i18n')] +add-translation locale: _check-pyside-setup _prepare-translation-extractions + @cd {{ DIRECTORY_BUILD_TRANSLATIONS }}; \ + {{ TOOL_CLI_LUPDATE }} \ + -verbose \ + -source-language en_US \ + -target-language {{ locale }} \ + -ts {{ DIRECTORY_I18N }}/{{ locale }}.ts + @echo '' + @just update-translations + +# Update *.ts files by traversing the source code +[group('i18n')] +update-translations: _check-pyside-setup _clean-develop _prepare-translation-extractions + @# Traverses *.qml and *.py files to update translation files + @# Requires translations in .py: QCoreApplication.translate("context", "string") + @# Requires translations in .qml: qsTranslate("context", "string") + + @cd {{ DIRECTORY_BUILD_TRANSLATIONS }}; \ + {{ TOOL_CLI_LUPDATE }} \ + -locations none \ + -project {{ FILE_BUILD_TRANSLATIONS_JSON }} + @cp -r \ + {{ DIRECTORY_BUILD_TRANSLATIONS }}/{{ NAME_DIRECTORY_I18N }}/*.ts \ + {{ DIRECTORY_I18N }} + +# Run Python and QML tests +[group('test')] +test: test-python test-qml + +# Run Python tests +[group('test')] +test-python: _check-pyside-setup _clean-test _compile-resources + @cp \ + {{ FILE_BUILD_RESOURCES }} \ + {{ FILE_PY_TEST_RESOURCES }} + @{{ PYTHON }} -m \ + pytest test + +# Run QML tests +[group('test')] +test-qml: _check-qml-setup + @{{ TOOL_CLI_QML_TESTRUNNER }} \ + -silent \ + -input {{ DIRECTORY_QML_TESTS }} + +_clean-build: + @rm -rf \ + {{ DIRECTORY_BUILD }} + +_clean-develop: + @rm -rf \ + {{ FILE_PY_SOURCES_RESOURCES }} + +_clean-test: + @rm -rf \ + {{ FILE_PY_TEST_RESOURCES }} + +_check-pyside-setup: + @which {{ PYTHON }} + @which {{ TOOL_CLI_LUPDATE }} + @which {{ TOOL_CLI_LRELEASE }} + @which {{ TOOL_CLI_RCC }} + @echo '' + +_check-qml-setup: + @which {{ TOOL_CLI_QML_TESTRUNNER }} + @echo '' + +_compile-resources: _generate-qrc-data _generate-qrc-i18n _generate-qrc-qml + @rm -rf \ + {{ DIRECTORY_BUILD_RESOURCES }} + @mkdir -p \ + {{ DIRECTORY_BUILD_RESOURCES }} + @cp -r \ + {{ DIRECTORY_BUILD_QRC_QML }}/. \ + {{ DIRECTORY_BUILD_QRC_DATA }}/. \ + {{ DIRECTORY_BUILD_QRC_I18N }}/. \ + {{ DIRECTORY_BUILD_RESOURCES }} + @{{ TOOL_CLI_RCC }} \ + {{ DIRECTORY_BUILD_RESOURCES }}/{{ NAME_DIRECTORY_DATA }}.qrc \ + {{ DIRECTORY_BUILD_RESOURCES }}/{{ NAME_DIRECTORY_I18N }}.qrc \ + {{ DIRECTORY_BUILD_RESOURCES }}/{{ NAME_DIRECTORY_QML_SOURCES }}.qrc \ + -o {{ FILE_BUILD_RESOURCES }} + +_generate-qrc-data: + @rm -rf \ + {{ DIRECTORY_BUILD_QRC_DATA }} + @mkdir -p \ + {{ DIRECTORY_BUILD_QRC_DATA }} + @cp -r \ + {{ DIRECTORY_DATA }} \ + {{ DIRECTORY_BUILD_QRC_DATA }} + @cd \ + {{ DIRECTORY_BUILD_QRC_DATA }}/{{ NAME_DIRECTORY_DATA }}; \ + {{ TOOL_CLI_RCC }} \ + --project | sed 's,./,{{ NAME_DIRECTORY_DATA }}/,' > {{ FILE_BUILD_QRC_DATA }} + +_generate-qrc-i18n: + @rm -rf \ + {{ DIRECTORY_BUILD_QRC_I18N }} + @mkdir -p \ + {{ DIRECTORY_BUILD_QRC_I18N }} + @cp -r \ + {{ DIRECTORY_I18N }} {{ DIRECTORY_BUILD_QRC_I18N }} + @{{ DIRECTORY_BUILD_HELPERS }}/generate-lupdate-project-file.py \ + --relative-to {{ DIRECTORY_BUILD_QRC_I18N }} \ + --out-file {{ FILE_BUILD_QRC_I18N_JSON }} + @cd \ + {{ DIRECTORY_BUILD_QRC_I18N }}; \ + {{ TOOL_CLI_LRELEASE }} \ + -project {{ FILE_BUILD_QRC_I18N_JSON }} + @cd \ + {{ DIRECTORY_BUILD_QRC_I18N }}/{{ NAME_DIRECTORY_I18N }}; \ + rm \ + {{ FILE_BUILD_QRC_I18N_JSON }} \ + *.ts + @cd \ + {{ DIRECTORY_BUILD_QRC_I18N }}/{{ NAME_DIRECTORY_I18N }}; \ + {{ TOOL_CLI_RCC }} \ + --project | sed 's,./,{{ NAME_DIRECTORY_I18N }}/,' > {{ FILE_BUILD_QRC_I18N }} + +_generate-qrc-qml: + @rm -rf \ + {{ DIRECTORY_BUILD_QRC_QML }} + @mkdir -p \ + {{ DIRECTORY_BUILD_QRC_QML }} + @cp -r \ + {{ DIRECTORY_QML_SOURCES }} \ + {{ DIRECTORY_BUILD_QRC_QML }} + @cd \ + {{ DIRECTORY_BUILD_QRC_QML }}/{{ NAME_DIRECTORY_QML_SOURCES }}; \ + {{ TOOL_CLI_RCC }} \ + --project | sed 's,./,{{ NAME_DIRECTORY_QML_SOURCES }}/,' > {{ FILE_BUILD_QRC_QML }} + +_prepare-translation-extractions: + @rm -rf \ + {{ DIRECTORY_BUILD_TRANSLATIONS }} + @mkdir -p \ + {{ DIRECTORY_BUILD_TRANSLATIONS }} + @cp -r \ + {{ DIRECTORY_I18N }} \ + {{ DIRECTORY_PY_SOURCES }} \ + {{ DIRECTORY_QML_SOURCES }} \ + {{ DIRECTORY_BUILD_TRANSLATIONS }} + @{{ DIRECTORY_BUILD_HELPERS }}/generate-lupdate-project-file.py \ + --relative-to {{ DIRECTORY_BUILD_TRANSLATIONS }} \ + --out-file {{ FILE_BUILD_TRANSLATIONS_JSON }} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.MD b/README.MD new file mode 100644 index 00000000..2e996851 --- /dev/null +++ b/README.MD @@ -0,0 +1,85 @@ +# PySide6 + QtQuick Project Template (Unofficial) + +Unofficial opinionated project template to get started with PySide6 and QtQuick quickly without worrying about tooling. + +![screenshot](docs/picture.png) + +# Features + +* Compatible with Python **3.9+** +* Internationalization including (LTR/RTL) +* Resources compiled ahead of time: + * Everything in `data`, `i18n`, and `qml` will be compiled into a Python file + * Final build only consists of Python files +* Testing preconfigured (Python + Qml) +* CI preconfigured +* Client side window decorations implemented +* Qt creator not required (use your favorite text editor) + +# Development Setup + +## Quick Start + +This project assumes that a virtual environment is used. + +1. Set up the development environment for your OS + * [Linux](docs/dev-setup-linux.md) + * [Windows](docs/dev-setup-windows.md) +2. Run `just build-develop` to compile resources +3. Run `python main.py` to start the app + +## Just recipes + +```just +$ just --list +Available recipes: + [build] + build # Build full project into build/release + build-develop # Build and compile resources into source directory + clean # Remove ALL generated files + + [i18n] + add-translation locale # Add new language + update-translations # Update *.ts files by traversing the source code + + [test] + test # Run Python and QML tests + test-python # Run Python tests + test-qml # Run QML tests +``` + +## Workflow + +Run `just build-develop` after each change in the `data`, `i18n`, or `qml` directories. +This will *compile* everything into a Python file and move it into the `myapp` directory +where it will be picked up on app start. + +## Internationalization + +* Adding new languages is described [here](docs/internationalization.md) + +## Read Further + +* Qt6: https://doc.qt.io +* Python: https://www.python.org +* PySide6: https://doc.qt.io/qtforpython/contents.html +* QML Coding Conventions: https://doc.qt.io/qt-6/qml-codingconventions.html +* Python & Qml: https://doc.qt.io/qtforpython/PySide6/QtQml/index.html +* Scripting: https://doc.qt.io/qt-6/topics-scripting.html +* Importing JavaScript Resources in QML: https://doc.qt.io/qt-6/qtqml-javascript-imports.html +* Qt, QtQuick & Python examples are located in `venv/lib//site-packages/PySide6/examples` + after dev environment is set up completely + +# Dependencies + +* PySide6 https://pypi.org/project/PySide6 +* PyTest https://pypi.org/project/pytest +* Just https://github.com/casey/just + +* App Icon: https://commons.wikimedia.org/wiki/File:Qt_logo_2016.svg +* Material Icons: https://fonts.google.com/icons?selected=Material+Icons + +# FAQ + +* Only **PySide6**? Can I substitute **PySide6** with **PyQt6**? + > No. Resources will be compiled ahead of time and PyQt6 dropped support for this. diff --git a/build-aux/generate-lupdate-project-file.py b/build-aux/generate-lupdate-project-file.py new file mode 100755 index 00000000..43cfe32b --- /dev/null +++ b/build-aux/generate-lupdate-project-file.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import argparse +import json +import sys +from pathlib import Path + + +class ArgumentValidator: + _errors = [] + + def validate_directory(self, directory: Path, *, name: str): + if not directory.exists(): + self._errors.append(f'{name.capitalize()} {directory} does not exist') + elif not directory.is_dir(): + self._errors.append(f'{name.capitalize()} {directory} is not a directory') + + def break_on_errors(self): + if errors := self._errors: + for error in errors: + print(error, file=sys.stderr) + sys.exit(1) + + +class ProjectFileGenerator: + _extensions_ignored = {'.pyc'} + _extensions_translation = '.ts' + _files = [] + + def __init__(self, root_dir: Path): + self._root_dir = root_dir + + def glob_files(self): + self._files = [path for path in self._root_dir.rglob('*') if path.is_file()] + + def make_files_relative(self): + self._files = [path.relative_to(self._root_dir) for path in self._files] + + def remove_irrelevant_files(self): + self._files = [path for path in self._files if path.suffix not in self._extensions_ignored] + + def sort_files(self): + self._files = sorted(self._files) + + def generate_project_file(self, file: Path): + files = [str(path) for path in self._files if path.suffix != self._extensions_translation] + translations = [str(path) for path in self._files if path.suffix == self._extensions_translation] + structure = { + 'excluded': [], + 'includePaths': [], + 'projectFile': '', + 'sources': files, + 'translations': translations, + } + data = json.dumps([structure], indent=2, sort_keys=True) + file.write_text(data, encoding='utf-8') + + +def main(): + parser = argparse.ArgumentParser(description='Create a json project file') + parser.add_argument('--relative-to', type=str, required=True, + help='Root directory to look for files') + parser.add_argument('--out-file', type=str, required=True, + help='Path of the json project file to generate') + run(parser.parse_args()) + + +def run(args): + root_dir = Path(args.relative_to).absolute() + out_file = Path(args.out_file) + + validator = ArgumentValidator() + validator.validate_directory(root_dir, name='Root directory') + validator.break_on_errors() + + generator = ProjectFileGenerator(root_dir=root_dir) + generator.glob_files() + generator.make_files_relative() + generator.remove_irrelevant_files() + generator.sort_files() + generator.generate_project_file(file=out_file) + + +if __name__ == '__main__': + main() diff --git a/build-aux/generate-qt-creator-project-file.py b/build-aux/generate-qt-creator-project-file.py new file mode 100644 index 00000000..488cae22 --- /dev/null +++ b/build-aux/generate-qt-creator-project-file.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import argparse +import json +import sys +from pathlib import Path + + +class ArgumentValidator: + _errors = [] + + def validate_directory(self, directory: Path): + if not directory.exists(): + self._errors.append(f'Directory {directory} does not exist') + elif not directory.is_dir(): + self._errors.append(f'Directory {directory} is not a directory') + + def validate_directories(self, directories: list[Path]): + for directory in directories: + self.validate_directory(directory) + + def validate_files(self, files: list[Path]): + for file in files: + self._validate_file(file) + + def _validate_file(self, file: Path): + if not file.exists(): + self._errors.append(f'File {file} does not exist') + elif not file.is_file(): + self._errors.append(f'File {file} is not a file') + + def break_on_errors(self): + if errors := self._errors: + for error in errors: + print(error, file=sys.stderr) + sys.exit(1) + + +class ProjectFileGenerator: + _extensions_ignored = {'.pyc'} + _files = [] + + def __init__(self, root_dir: Path): + self._root_dir = root_dir + + def add(self, directories: list[Path], files: list[Path]): + for directory in directories: + for path in directory.rglob('*'): + if path.is_file(): + self._files.append(path) + self._files.extend(files) + + def remove_irrelevant_files(self): + self._files = [path for path in self._files if path.suffix not in self._extensions_ignored] + + def make_files_relative(self): + self._files = [path.relative_to(self._root_dir) for path in self._files] + + def sort_files(self): + self._files = sorted(self._files) + + def generate_project_file(self, output: Path): + structure = {'files': [str(file) for file in self._files]} + data = json.dumps(structure, indent=2, sort_keys=True) + output.write_text(data, encoding='utf-8') + + +def main(): + parser = argparse.ArgumentParser(description='Create a pyproject file') + parser.add_argument('--relative-to', type=str, required=True, + help='Root directory to make files relative to') + parser.add_argument('--include-directory', type=str, action='append', default=[], + help='Directory to include. Can be used multiple times') + parser.add_argument('--include-file', type=str, action='append', default=[], + help='File to include. Can be used multiple times') + parser.add_argument('--out-file', type=str, required=True, + help='Path of the pyproject file to generate') + run(parser.parse_args()) + + +def run(args): + root_dir = Path(args.relative_to).absolute() + out_file = Path(args.out_file) + directories = [Path(path).absolute() for path in args.include_directory] + files = [Path(path).absolute() for path in args.include_file] + + validator = ArgumentValidator() + validator.validate_directory(root_dir) + validator.validate_directories(directories) + validator.validate_files(files) + validator.break_on_errors() + + generator = ProjectFileGenerator(root_dir) + generator.add(directories, files) + generator.remove_irrelevant_files() + generator.make_files_relative() + generator.sort_files() + generator.generate_project_file(output=out_file) + + +if __name__ == '__main__': + main() diff --git a/build-aux/linux/Justfile b/build-aux/linux/Justfile new file mode 100755 index 00000000..8738a22a --- /dev/null +++ b/build-aux/linux/Justfile @@ -0,0 +1,12 @@ +#!/usr/bin/env just --justfile + +@_default: + just --list + +# Prints relevant info for flatpak pypi dependencies +print: + @python flatpak-pypi-checker.py \ + --dependency PySide6-Essentials==6.8.0::manylinux:x86_64 \ + --dependency shiboken6==6.8.0::manylinux:x86_64 \ + | jq + diff --git a/build-aux/linux/flatpak-pypi-checker.py b/build-aux/linux/flatpak-pypi-checker.py new file mode 100644 index 00000000..82c3cd4b --- /dev/null +++ b/build-aux/linux/flatpak-pypi-checker.py @@ -0,0 +1,120 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import json +import re +import urllib.request +from dataclasses import dataclass + + +class RequirementsUpdater: + """""" + + @dataclass(frozen=True) + class Requirement: + name: str + version: str + filters: list[str] # filter1:filter2:filter3 + data: list = None + + _requirements: dict[str, Requirement] = {} # name mapped to object + + @property + def requirements(self) -> dict[str, Requirement]: + return dict(self._requirements) + + def configure_for(self, dependencies: list[str]) -> None: + for dependency in dependencies: + name, filters = dependency.split("::") + filters = [f.strip() for f in filters.split(":")] + + if "==" in name: + name, _, version = re.split("(>=|==|<=)", name) + elif "<=" in name: + raise ValueError("Version requirement '<=' currently not supported") + else: + version = "latest" + + self._requirements[name] = self.Requirement(name, version, filters) + + def resolve(self) -> None: + for requirement in self._requirements.values(): + name = requirement.name + + with urllib.request.urlopen(f"https://pypi.org/pypi/{name}/json", timeout=5) as connection: + data = json.loads(connection.read().decode("utf-8").strip()) + + version = data["info"]["version"] if requirement.version == "latest" else requirement.version + filters = requirement.filters + data = data["releases"][version] + + self._requirements[requirement.name] = self.Requirement(name, version, filters, data) + + def extract(self) -> list: + """""" + + def find_first_filename_matching(files: list, must_contain_substr: list[str]) -> dict: + for file in files: + if all(f in file["filename"] for f in must_contain_substr): + return file + raise StopIteration( + f"Cannot find file containing all required substrings: {", ".join(must_contain_substr)}" + ) + + dependencies = [] + + for requirement in self._requirements.values(): + filters = requirement.filters + release = find_first_filename_matching(files=requirement.data, must_contain_substr=filters) + value = { + "name": requirement.name, + "version": requirement.version, + "filename": release["filename"], + "sha256": release["digests"]["sha256"], + "url": release["url"], + } + dependencies.append(value) + + dependencies.sort(key=lambda x: x["name"]) + + return dependencies + + +def main(): + parser = argparse.ArgumentParser(description="Prints relevant info for flatpak pypi dependencies") + parser.add_argument( + "--dependency", + type=str, + action="append", + default=[], + help="dependency to consider: dependency::filename-filter1:filename-filter2", + ) + run(parser.parse_args()) + + +def run(args): + updater = RequirementsUpdater() + updater.configure_for(args.dependency) + updater.resolve() + + dependencies = updater.extract() + data = json.dumps(dependencies) + + print(data) + + +if __name__ == "__main__": + main() diff --git a/build-aux/linux/krb5.conf b/build-aux/linux/krb5.conf new file mode 100644 index 00000000..e0d80d36 --- /dev/null +++ b/build-aux/linux/krb5.conf @@ -0,0 +1,9 @@ +[libdefaults] +dns_lookup_realm = false +ticket_lifetime = 24h +renew_lifetime = 7d +forwardable = true +rdns = false +pkinit_anchors = FILE:/etc/ssl/certs/ca-certificates.crt +spake_preauth_groups = edwards25519 +default_ccache_name = KCM: diff --git a/build-aux/linux/org.github.trin94.pyside6.template.desktop b/build-aux/linux/org.github.trin94.pyside6.template.desktop new file mode 100644 index 00000000..52fe6a30 --- /dev/null +++ b/build-aux/linux/org.github.trin94.pyside6.template.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=PySide6-Template +Icon=org.github.trin94.pyside6.template.svg +Exec=run-my-app +Terminal=false +Type=Application +Categories=Utility +StartupNotify=true diff --git a/build-aux/linux/org.github.trin94.pyside6.template.metainfo.xml b/build-aux/linux/org.github.trin94.pyside6.template.metainfo.xml new file mode 100644 index 00000000..bed31f8e --- /dev/null +++ b/build-aux/linux/org.github.trin94.pyside6.template.metainfo.xml @@ -0,0 +1,28 @@ + + + org.github.trin94.pyside6.template.desktop + CC0-1.0 + LGPL-3.0-or-later + PySide6-Template + Summary + +

Description

+
+ + pointing + keyboard + touch + + + + + What's new? + + + + run-my-app + + https://github.com/trin94/PySide6-project-template + https://github.com/trin94/PySide6-project-template/issues + elias.mr1@gmail.com +
diff --git a/build-aux/linux/org.github.trin94.pyside6.template.svg b/build-aux/linux/org.github.trin94.pyside6.template.svg new file mode 100644 index 00000000..13163453 --- /dev/null +++ b/build-aux/linux/org.github.trin94.pyside6.template.svg @@ -0,0 +1,3 @@ + + + diff --git a/build-aux/linux/org.github.trin94.pyside6.template.yml b/build-aux/linux/org.github.trin94.pyside6.template.yml new file mode 100644 index 00000000..b110daa8 --- /dev/null +++ b/build-aux/linux/org.github.trin94.pyside6.template.yml @@ -0,0 +1,110 @@ +# Supposed to be called at repository root level - copied during CI build to root level +# +# To build: +# just +# flatpak-builder build-dir org.github.trin94.pyside6.template.yml --force-clean +# flatpak-builder --run build-dir org.github.trin94.pyside6.template.yml run-my-app + +app-id: org.github.trin94.pyside6.template +runtime: org.freedesktop.Platform +runtime-version: '23.08' +sdk: org.freedesktop.Sdk +command: run-my-app + +finish-args: + - --share=ipc + - --socket=wayland + - --socket=fallback-x11 + - --socket=pulseaudio + - --filesystem=host + - --device=dri + +build-options: + cflags: -O2 -g + cxxflags: -O2 -g + env: + V: '1' + +cleanup: + - /include + - /lib/debug + - /lib/pkgconfig + - /lib/python3.12/site-packages/PySide6/examples + - /lib/python3.12/site-packages/PySide6/lupdate + - /lib/python3.12/site-packages/PySide6/assistant + - /lib/python3.12/site-packages/PySide6/qmllint + - /lib/python3.12/site-packages/PySide6/linguist + - /lib/python3.12/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6 + - /lib/python3.12/site-packages/PySide6/Qt/translations/qtwebengine_locales + - /lib/python3.12/site-packages/PySide6/Qt/resources + - /man + - /share/doc + - /share/gtk-doc + - /share/man + - '*.la' + - '*.a' + +modules: + - name: python + sources: + - type: archive + url: https://www.python.org/ftp/python/3.12.7/Python-3.12.7.tar.xz + sha256: 24887b92e2afd4a2ac602419ad4b596372f67ac9b077190f459aba390faf5550 + + # https://github.com/flathub/io.qt.qtwebengine.BaseApp/tree/branch/6.7/krb5 + - name: krb5 + subdir: src + cleanup: + - /bin + - /share/et + - /share/examples + - /share/man + config-opts: + - --localstatedir=/var/lib + - --sbindir=${FLATPAK_DEST}/bin + - --disable-rpath + - --disable-static + post-install: + - install -Dm644 ../krb5.conf -t ${FLATPAK_DEST}/etc/ + sources: + - type: file + path: build-aux/linux/krb5.conf + - type: archive + url: https://kerberos.org/dist/krb5/1.21/krb5-1.21.2.tar.gz + sha256: 9560941a9d843c0243a71b17a7ac6fe31c7cebb5bce3983db79e52ae7e850491 + + - name: pypi-dependencies + buildsystem: simple + build-commands: [ ] + config-opts: + - --force-clean + modules: + - name: python3-PySide6 + buildsystem: simple + build-commands: + - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" + --prefix=${FLATPAK_DEST} "PySide6-Essentials>=6.8.0" --no-build-isolation + sources: + - type: file + url: https://files.pythonhosted.org/packages/3b/92/9fa08c01ba811e2405059239e16d21b2e5c40b39de204c88fc35ac01c4ff/shiboken6-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl + sha256: ad88c0e73c9e4de3723c6e6b846e651729433ff9d9086bb2b4e6d49965477d97 + - type: file + url: https://files.pythonhosted.org/packages/9d/fd/17510a0abd503a904ce3b9f1af87385435cf9340fb79c020a53d3a8385a5/PySide6_Essentials-6.8.0-cp39-abi3-manylinux_2_28_x86_64.whl + sha256: da99a94806416ec1e386426a474e7d1e514c1cdf8ad171c005376f4f633e7216 + + - name: myapp + buildsystem: simple + build-commands: + - mkdir -p /app/app-directory + - cp -r myapp /app/app-directory + - install -D main.py /app/app-directory + - install -Dm644 org.github.trin94.pyside6.template.svg /app/share/icons/hicolor/scalable/apps/org.github.trin94.pyside6.template.svg + - install -Dm644 org.github.trin94.pyside6.template.desktop /app/share/applications/org.github.trin94.pyside6.template.desktop + - install -Dm644 org.github.trin94.pyside6.template.metainfo.xml -t /app/share/metainfo + - sed -i 's|/usr/bin/env python|/app/bin/python3|' /app/app-directory/main.py + - ln -s /app/app-directory/main.py /app/bin/run-my-app + sources: + - type: dir + path: build/release + - type: dir + path: build-aux/linux diff --git a/build-aux/windows/icon.ico b/build-aux/windows/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..39edede4a1f59d151c7ad51827d4c5754fde7f63 GIT binary patch literal 6819 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 T+y!s`mjL|-M%uNS_R;?X4+!PQ literal 0 HcmV?d00001 diff --git a/data/app-icon.svg b/data/app-icon.svg new file mode 100644 index 00000000..cb8989bb --- /dev/null +++ b/data/app-icon.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/close_black_24dp.svg b/data/icons/close_black_24dp.svg new file mode 100644 index 00000000..5f1267d7 --- /dev/null +++ b/data/icons/close_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/close_fullscreen_black_24dp.svg b/data/icons/close_fullscreen_black_24dp.svg new file mode 100644 index 00000000..6c9d1b12 --- /dev/null +++ b/data/icons/close_fullscreen_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/minimize_black_24dp.svg b/data/icons/minimize_black_24dp.svg new file mode 100644 index 00000000..fd35c432 --- /dev/null +++ b/data/icons/minimize_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/open_in_full_black_24dp.svg b/data/icons/open_in_full_black_24dp.svg new file mode 100644 index 00000000..86df2c53 --- /dev/null +++ b/data/icons/open_in_full_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/qtquickcontrols2.conf b/data/qtquickcontrols2.conf new file mode 100644 index 00000000..a94c63b9 --- /dev/null +++ b/data/qtquickcontrols2.conf @@ -0,0 +1,7 @@ +[Controls] +Style=Material + +[Material] +Variant=Dense +Accent=LightGreen +Theme=Dark diff --git a/docs/dev-setup-linux.md b/docs/dev-setup-linux.md new file mode 100644 index 00000000..bab06907 --- /dev/null +++ b/docs/dev-setup-linux.md @@ -0,0 +1,14 @@ +# Development Setup - Linux + +## Tools + +* Make sure `python` is on version **3.8** or later +* Install `just` (https://github.com/casey/just) + +## Checkout + +1. Clone this repository +1. Set up virtual environment: + 1. `python -m venv venv` + 1. `source venv/bin/activate` (*bash*) or `source venv/bin/activate.fish` (*fish*) +1. Install requirements `python -m pip install -r requirements.txt` diff --git a/docs/dev-setup-windows.md b/docs/dev-setup-windows.md new file mode 100644 index 00000000..20f2301f --- /dev/null +++ b/docs/dev-setup-windows.md @@ -0,0 +1,18 @@ +# Development Setup - Windows + +## Tools + +1. Install [python v3.8+](https://www.python.org/downloads/) +1. Install [git-bash](https://git-scm.com/downloads) +1. Install [just](https://github.com/casey/just) + +**just is supposed to be used from git-bash** + +## Checkout + +1. Clone this repository +1. Open git-bash in the directory +1. Set up virtual environment: + 1. `python -m venv venv` + 1. `source venv/Scripts/activate` +1. Install requirements `python -m pip install -r requirements.txt` diff --git a/docs/internationalization.md b/docs/internationalization.md new file mode 100644 index 00000000..79d76bfb --- /dev/null +++ b/docs/internationalization.md @@ -0,0 +1,16 @@ +# Adding Languages + +* Checkout repository +* Make sure development environment is set up correctly for your OS +* Create a new translation file by running + ```shell + just add-translation # just add-translation fr_FR + ``` +* New `.ts` file appears in the `i18n` directory +* Translate the `ts` file using Qt Linguist 6 +* To test the translation: + * Add a new entry in the `MyAppLanguageModel.qml` file + * Run + ```shell + just build-develop + ``` diff --git a/docs/picture.png b/docs/picture.png new file mode 100644 index 0000000000000000000000000000000000000000..107acfcff54993ee59b17ea77d6a53c027206b00 GIT binary patch literal 42451 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 L`dr)JzrFKMEg8~7 literal 0 HcmV?d00001 diff --git a/i18n/de_DE.ts b/i18n/de_DE.ts new file mode 100644 index 00000000..6f0a9af0 --- /dev/null +++ b/i18n/de_DE.ts @@ -0,0 +1,88 @@ + + + + + HeaderBar + + &Help + + + + &Action 1 + + + + &Action 2 + + + + &Action 3 + + + + &Menu 1 + + + + &Action 4 + + + + &Action 5 + + + + &Menu 2 + + + + &Options + + + + &Language + + + + Showcase translated Qt internal strings + + + + + Languages + + English + Englisch + + + German + Deutsch + + + Hebrew + Hebräisch + + + + MainPage + + Have fun! + Viel Spaß! + + + Exposed from Python: '%1' + + + + + MessageBoxes + + Title + + + + Change the language and look at the 'Yes' and 'Cancel' buttons + + + + diff --git a/i18n/he_IL.ts b/i18n/he_IL.ts new file mode 100644 index 00000000..962fd772 --- /dev/null +++ b/i18n/he_IL.ts @@ -0,0 +1,88 @@ + + + + + HeaderBar + + &Help + + + + &Action 1 + + + + &Action 2 + + + + &Action 3 + + + + &Menu 1 + + + + &Action 4 + + + + &Action 5 + + + + &Menu 2 + + + + &Options + + + + &Language + + + + Showcase translated Qt internal strings + + + + + Languages + + English + אנגלית + + + German + גֶרמָנִיָת + + + Hebrew + עִברִית + + + + MainPage + + Have fun! + תעשה חיים! + + + Exposed from Python: '%1' + + + + + MessageBoxes + + Title + + + + Change the language and look at the 'Yes' and 'Cancel' buttons + + + + diff --git a/main.py b/main.py new file mode 100755 index 00000000..f6a4c514 --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + + +def main(): + from myapp.startup import perform_startup + + perform_startup() + + +if __name__ == '__main__': + main() diff --git a/myapp/__init__.py b/myapp/__init__.py new file mode 100644 index 00000000..398a9d14 --- /dev/null +++ b/myapp/__init__.py @@ -0,0 +1,14 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/myapp/application.py b/myapp/application.py new file mode 100644 index 00000000..c76df5b0 --- /dev/null +++ b/myapp/application.py @@ -0,0 +1,84 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import platform +import sys + +from PySide6.QtCore import QUrl, QTranslator, QLocale, QLibraryInfo +from PySide6.QtGui import QGuiApplication, QIcon +from PySide6.QtQml import QQmlApplicationEngine + + +class MyApplication(QGuiApplication): + + def __init__(self, args): + super().__init__(args) + self._engine = QQmlApplicationEngine() + self._translator_myapp = QTranslator() + self._translator_qt = QTranslator() + + def set_window_icon(self): + icon = QIcon(':/data/app-icon.svg') + self.setWindowIcon(icon) + + def set_up_signals(self): + self.aboutToQuit.connect(self._on_quit) + self._engine.uiLanguageChanged.connect(self._retranslate) + + def _on_quit(self) -> None: + del self._engine + + def _retranslate(self): + locale = QLocale(self._engine.uiLanguage()) + + self.removeTranslator(self._translator_qt) + self.removeTranslator(self._translator_myapp) + + self._translator_qt.load(locale, "qtbase", "_", QLibraryInfo.location(QLibraryInfo.TranslationsPath)) + self._translator_myapp.load(f':/i18n/{locale.name()}.qm') + + self.installTranslator(self._translator_qt) + self.installTranslator(self._translator_myapp) + + self.setLayoutDirection(locale.textDirection()) + + def set_up_imports(self): + self._engine.addImportPath(':/qml') + + def set_up_window_event_filter(self): + if platform.system() == "Windows": + from myapp.framelesswindow.win import WindowsEventFilter + self._event_filter = WindowsEventFilter(border_width=5) + self.installNativeEventFilter(self._event_filter) + elif platform.system() == 'Linux': + from myapp.framelesswindow.linux import LinuxEventFilter + self._event_filter = LinuxEventFilter(border_width=5) + self.installEventFilter(self._event_filter) + + def start_engine(self): + self._engine.load(QUrl.fromLocalFile(':/qml/main.qml')) + + def set_up_window_effects(self): + if sys.platform == 'win32': + hwnd = self.topLevelWindows()[0].winId() + from myapp.framelesswindow.win import WindowsWindowEffect + self._effects = WindowsWindowEffect() + self._effects.addShadowEffect(hwnd) + self._effects.addWindowAnimation(hwnd) + + def verify(self): + if not self._engine.rootObjects(): + sys.exit(-1) diff --git a/myapp/framelesswindow/__init__.py b/myapp/framelesswindow/__init__.py new file mode 100644 index 00000000..398a9d14 --- /dev/null +++ b/myapp/framelesswindow/__init__.py @@ -0,0 +1,14 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/myapp/framelesswindow/linux/__init__.py b/myapp/framelesswindow/linux/__init__.py new file mode 100644 index 00000000..501c1359 --- /dev/null +++ b/myapp/framelesswindow/linux/__init__.py @@ -0,0 +1,16 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .event import LinuxEventFilter diff --git a/myapp/framelesswindow/linux/event.py b/myapp/framelesswindow/linux/event.py new file mode 100644 index 00000000..99ed3003 --- /dev/null +++ b/myapp/framelesswindow/linux/event.py @@ -0,0 +1,69 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Inspired and based on: +# - https://github.com/zhiyiYo/PyQt-Frameless-Window +# - https://gitee.com/Virace/pyside6-qml-frameless-window/tree/main + + +from typing import Optional + +from PySide6.QtCore import QCoreApplication, QObject, QEvent, Qt +from PySide6.QtGui import QCursor, QGuiApplication, QWindow + + +class LinuxEventFilter(QObject): + + def __init__(self, border_width=None) -> None: + super().__init__() + self.border_width = border_width + + self._app: QGuiApplication = QCoreApplication.instance() + self._window: Optional[QWindow] = None + + def eventFilter(self, obj, event): + if event.type() != QEvent.MouseButtonPress and event.type() != QEvent.MouseMove: + return False + + if self._window is None: + self._window = self._app.topLevelWindows()[0] + + pos = QCursor.pos() - self._window.position() + edges = Qt.Edge(0) + if pos.x() < self.border_width: + edges |= Qt.LeftEdge + if pos.x() >= self._window.width() - self.border_width: + edges |= Qt.RightEdge + if pos.y() < self.border_width: + edges |= Qt.TopEdge + if pos.y() >= self._window.height() - self.border_width: + edges |= Qt.BottomEdge + + if event.type() == QEvent.MouseMove and self._window.windowState() == Qt.WindowNoState: + if edges in (Qt.LeftEdge | Qt.TopEdge, Qt.RightEdge | Qt.BottomEdge): + self._app.setOverrideCursor(Qt.SizeFDiagCursor) + elif edges in (Qt.RightEdge | Qt.TopEdge, Qt.LeftEdge | Qt.BottomEdge): + self._app.setOverrideCursor(Qt.SizeBDiagCursor) + elif edges in (Qt.TopEdge, Qt.BottomEdge): + self._app.setOverrideCursor(Qt.SizeVerCursor) + elif edges in (Qt.LeftEdge, Qt.RightEdge): + self._app.setOverrideCursor(Qt.SizeHorCursor) + else: + self._app.restoreOverrideCursor() + + if event.type() == QEvent.MouseButtonPress and edges: + self._window.startSystemResize(edges) + return True + + return super().eventFilter(obj, event) diff --git a/myapp/framelesswindow/win/__init__.py b/myapp/framelesswindow/win/__init__.py new file mode 100644 index 00000000..40c0b085 --- /dev/null +++ b/myapp/framelesswindow/win/__init__.py @@ -0,0 +1,22 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Inspired and based on: +# - https://github.com/zhiyiYo/PyQt-Frameless-Window +# - https://gitee.com/Virace/pyside6-qml-frameless-window/tree/main + + +from .effect import WindowsWindowEffect +from .event import WindowsEventFilter diff --git a/myapp/framelesswindow/win/c_structures.py b/myapp/framelesswindow/win/c_structures.py new file mode 100644 index 00000000..bf2f181b --- /dev/null +++ b/myapp/framelesswindow/win/c_structures.py @@ -0,0 +1,172 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# Inspired and based on: +# - https://github.com/zhiyiYo/PyQt-Frameless-Window +# - https://gitee.com/Virace/pyside6-qml-frameless-window/tree/main + + +from ctypes import POINTER, Structure, c_int +from ctypes.wintypes import DWORD, HWND, ULONG, POINT, RECT, UINT, BOOL, HRGN +from enum import Enum + + +class WINDOWCOMPOSITIONATTRIB(Enum): + WCA_UNDEFINED = 0 + WCA_NCRENDERING_ENABLED = 1 + WCA_NCRENDERING_POLICY = 2 + WCA_TRANSITIONS_FORCEDISABLED = 3 + WCA_ALLOW_NCPAINT = 4 + WCA_CAPTION_BUTTON_BOUNDS = 5 + WCA_NONCLIENT_RTL_LAYOUT = 6 + WCA_FORCE_ICONIC_REPRESENTATION = 7 + WCA_EXTENDED_FRAME_BOUNDS = 8 + WCA_HAS_ICONIC_BITMAP = 9 + WCA_THEME_ATTRIBUTES = 10 + WCA_NCRENDERING_EXILED = 11 + WCA_NCADORNMENTINFO = 12 + WCA_EXCLUDED_FROM_LIVEPREVIEW = 13 + WCA_VIDEO_OVERLAY_ACTIVE = 14 + WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15 + WCA_DISALLOW_PEEK = 16 + WCA_CLOAK = 17 + WCA_CLOAKED = 18 + WCA_ACCENT_POLICY = 19 + WCA_FREEZE_REPRESENTATION = 20 + WCA_EVER_UNCLOAKED = 21 + WCA_VISUAL_OWNER = 22 + WCA_HOLOGRAPHIC = 23 + WCA_EXCLUDED_FROM_DDA = 24 + WCA_PASSIVEUPDATEMODE = 25 + WCA_USEDARKMODECOLORS = 26 + WCA_CORNER_STYLE = 27 + WCA_PART_COLOR = 28 + WCA_DISABLE_MOVESIZE_FEEDBACK = 29 + WCA_LAST = 30 + + +class ACCENT_STATE(Enum): + """ Client area status enumeration class """ + ACCENT_DISABLED = 0 + ACCENT_ENABLE_GRADIENT = 1 + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2 + ACCENT_ENABLE_BLURBEHIND = 3 # Aero effect + ACCENT_ENABLE_ACRYLICBLURBEHIND = 4 # Acrylic effect + ACCENT_ENABLE_HOSTBACKDROP = 5 # Mica effect + ACCENT_INVALID_STATE = 6 + + +class ACCENT_POLICY(Structure): + """ Specific attributes of client area """ + + _fields_ = [ + ("AccentState", DWORD), + ("AccentFlags", DWORD), + ("GradientColor", DWORD), + ("AnimationId", DWORD), + ] + + +class WINDOWCOMPOSITIONATTRIBDATA(Structure): + _fields_ = [ + ("Attribute", DWORD), + # Pointer() receives any ctypes type and returns a pointer type + ("Data", POINTER(ACCENT_POLICY)), + ("SizeOfData", ULONG), + ] + + +class DWMNCRENDERINGPOLICY(Enum): + DWMNCRP_USEWINDOWSTYLE = 0 + DWMNCRP_DISABLED = 1 + DWMNCRP_ENABLED = 2 + DWMNCRP_LAS = 3 + + +class DWMWINDOWATTRIBUTE(Enum): + DWMWA_NCRENDERING_ENABLED = 1 + DWMWA_NCRENDERING_POLICY = 2 + DWMWA_TRANSITIONS_FORCEDISABLED = 3 + DWMWA_ALLOW_NCPAINT = 4 + DWMWA_CAPTION_BUTTON_BOUNDS = 5 + DWMWA_NONCLIENT_RTL_LAYOUT = 6 + DWMWA_FORCE_ICONIC_REPRESENTATION = 7 + DWMWA_FLIP3D_POLICY = 8 + DWMWA_EXTENDED_FRAME_BOUNDS = 9 + DWMWA_HAS_ICONIC_BITMAP = 10 + DWMWA_DISALLOW_PEEK = 11 + DWMWA_EXCLUDED_FROM_PEEK = 12 + DWMWA_CLOAK = 13 + DWMWA_CLOAKED = 14 + DWMWA_FREEZE_REPRESENTATION = 15 + DWMWA_PASSIVE_UPDATE_MODE = 16 + DWMWA_USE_HOSTBACKDROPBRUSH = 17 + DWMWA_USE_IMMERSIVE_DARK_MODE = 18 + DWMWA_WINDOW_CORNER_PREFERENCE = 19 + DWMWA_BORDER_COLOR = 20 + DWMWA_CAPTION_COLOR = 21 + DWMWA_TEXT_COLOR = 22 + DWMWA_VISIBLE_FRAME_BORDER_THICKNESS = 23 + DWMWA_LAST = 24 + + +class MARGINS(Structure): + _fields_ = [ + ("cxLeftWidth", c_int), + ("cxRightWidth", c_int), + ("cyTopHeight", c_int), + ("cyBottomHeight", c_int), + ] + + +class MINMAXINFO(Structure): + _fields_ = [ + ("ptReserved", POINT), + ("ptMaxSize", POINT), + ("ptMaxPosition", POINT), + ("ptMinTrackSize", POINT), + ("ptMaxTrackSize", POINT), + ] + + +class PWINDOWPOS(Structure): + _fields_ = [ + ('hWnd', HWND), + ('hwndInsertAfter', HWND), + ('x', c_int), + ('y', c_int), + ('cx', c_int), + ('cy', c_int), + ('flags', UINT) + ] + + +class NCCALCSIZE_PARAMS(Structure): + _fields_ = [ + ('rgrc', RECT * 3), + ('lppos', POINTER(PWINDOWPOS)) + ] + + +LPNCCALCSIZE_PARAMS = POINTER(NCCALCSIZE_PARAMS) + + +class DWM_BLURBEHIND(Structure): + _fields_ = [ + ('dwFlags', DWORD), + ('fEnable', BOOL), + ('hRgnBlur', HRGN), + ('fTransitionOnMaximized', BOOL), + ] diff --git a/myapp/framelesswindow/win/effect.py b/myapp/framelesswindow/win/effect.py new file mode 100644 index 00000000..be0b24d9 --- /dev/null +++ b/myapp/framelesswindow/win/effect.py @@ -0,0 +1,86 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# Inspired and based on: +# - https://github.com/zhiyiYo/PyQt-Frameless-Window +# - https://gitee.com/Virace/pyside6-qml-frameless-window/tree/main + + +from ctypes import POINTER, byref, c_bool, c_int, pointer, sizeof, WinDLL, windll +from ctypes.wintypes import DWORD, LONG, LPCVOID + +import win32con +import win32gui + +from .c_structures import ACCENT_POLICY, MARGINS, WINDOWCOMPOSITIONATTRIB, WINDOWCOMPOSITIONATTRIBDATA, DWM_BLURBEHIND + + +class WindowsWindowEffect: + + def __init__(self): + self.user32 = WinDLL("user32") + self.dwmapi = WinDLL("dwmapi") + self.SetWindowCompositionAttribute = self.user32.SetWindowCompositionAttribute + self.DwmExtendFrameIntoClientArea = self.dwmapi.DwmExtendFrameIntoClientArea + self.DwmEnableBlurBehindWindow = self.dwmapi.DwmEnableBlurBehindWindow + self.DwmSetWindowAttribute = self.dwmapi.DwmSetWindowAttribute + + self.SetWindowCompositionAttribute.restype = c_bool + self.DwmExtendFrameIntoClientArea.restype = LONG + self.DwmEnableBlurBehindWindow.restype = LONG + self.DwmSetWindowAttribute.restype = LONG + + self.SetWindowCompositionAttribute.argtypes = [ + c_int, + POINTER(WINDOWCOMPOSITIONATTRIBDATA), + ] + self.DwmSetWindowAttribute.argtypes = [c_int, DWORD, LPCVOID, DWORD] + self.DwmExtendFrameIntoClientArea.argtypes = [c_int, POINTER(MARGINS)] + self.DwmEnableBlurBehindWindow.argtypes = [c_int, POINTER(DWM_BLURBEHIND)] + + # Initialize structure + self.accentPolicy = ACCENT_POLICY() + self.winCompAttrData = WINDOWCOMPOSITIONATTRIBDATA() + self.winCompAttrData.Attribute = WINDOWCOMPOSITIONATTRIB.WCA_ACCENT_POLICY.value + self.winCompAttrData.SizeOfData = sizeof(self.accentPolicy) + self.winCompAttrData.Data = pointer(self.accentPolicy) + + def addShadowEffect(self, hWnd): + if not self._isDwmCompositionEnabled(): + return + hWnd = int(hWnd) + margins = MARGINS(-1, -1, -1, -1) + self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) + + @staticmethod + def _isDwmCompositionEnabled(): + bResult = c_int(0) + windll.dwmapi.DwmIsCompositionEnabled(byref(bResult)) + return bool(bResult.value) + + @staticmethod + def addWindowAnimation(hWnd): + hWnd = int(hWnd) + style = win32gui.GetWindowLong(hWnd, win32con.GWL_STYLE) + win32gui.SetWindowLong( + hWnd, + win32con.GWL_STYLE, + style + | win32con.WS_MINIMIZEBOX + | win32con.WS_MAXIMIZEBOX + | win32con.WS_CAPTION + | win32con.CS_DBLCLKS + | win32con.WS_THICKFRAME, + ) diff --git a/myapp/framelesswindow/win/event.py b/myapp/framelesswindow/win/event.py new file mode 100644 index 00000000..226b2017 --- /dev/null +++ b/myapp/framelesswindow/win/event.py @@ -0,0 +1,131 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# Inspired and based on: +# - https://github.com/zhiyiYo/PyQt-Frameless-Window +# - https://gitee.com/Virace/pyside6-qml-frameless-window/tree/main + + +import ctypes.wintypes + +import PySide6.QtCore +import win32api +import win32con +import win32gui + +from .c_structures import MINMAXINFO, NCCALCSIZE_PARAMS + + +class WindowsEventFilter(PySide6.QtCore.QAbstractNativeEventFilter): + def __init__(self, border_width=None) -> None: + super().__init__() + self.border_width = border_width + self.monitor_info = None + + def nativeEventFilter(self, eventType, message): + msg = ctypes.wintypes.MSG.from_address(message.__int__()) + + if not msg.hWnd: + return False, 0 + + if msg.message == win32con.WM_NCHITTEST and (self.border_width is not None): + + x, y, w, h = self.get_window_size(msg.hWnd) + x_pos = (win32api.LOWORD(msg.lParam) - x) % 65536 + y_pos = win32api.HIWORD(msg.lParam) - y + + lx = x_pos < self.border_width + rx = x_pos + 9 > w - self.border_width + ty = y_pos < self.border_width + by = y_pos > h - self.border_width + + if lx and ty: + return True, win32con.HTTOPLEFT + elif rx and by: + return True, win32con.HTBOTTOMRIGHT + elif rx and ty: + return True, win32con.HTTOPRIGHT + elif lx and by: + return True, win32con.HTBOTTOMLEFT + elif ty: + return True, win32con.HTTOP + elif by: + return True, win32con.HTBOTTOM + elif lx: + return True, win32con.HTLEFT + elif rx: + return True, win32con.HTRIGHT + elif msg.message == win32con.WM_NCCALCSIZE: + if self.isWindowMaximized(msg.hWnd): + self.monitorNCCALCSIZE(msg) + return True, 0 + elif msg.message == win32con.WM_GETMINMAXINFO: + if self.isWindowMaximized(msg.hWnd): + window_rect = win32gui.GetWindowRect(msg.hWnd) + if not window_rect: + return False, 0 + + # get the monitor handle + monitor = win32api.MonitorFromRect(window_rect) + if not monitor: + return False, 0 + + # get the monitor info + self.monitor_info = win32api.GetMonitorInfo(monitor) + monitor_rect = self.monitor_info['Monitor'] + work_area = self.monitor_info['Work'] + + # convert lParam to MINMAXINFO pointer + info = ctypes.cast(msg.lParam, ctypes.POINTER(MINMAXINFO)).contents + + # adjust the size of window + info.ptMaxSize.x = work_area[2] - work_area[0] + info.ptMaxSize.y = work_area[3] - work_area[1] + info.ptMaxTrackSize.x = info.ptMaxSize.x + info.ptMaxTrackSize.y = info.ptMaxSize.y + + # modify the upper left coordinate + info.ptMaxPosition.x = abs(window_rect[0] - monitor_rect[0]) + info.ptMaxPosition.y = abs(window_rect[1] - monitor_rect[1]) + return True, 1 + return False, 0 + + @classmethod + def get_window_size(cls, hwnd): + left, top, right, bottom = win32gui.GetWindowRect(hwnd) + + width = right - left + height = bottom - top + return left, top, width, height + + def monitorNCCALCSIZE(self, msg: ctypes.wintypes.MSG): + monitor = win32api.MonitorFromWindow(msg.hWnd) + if monitor is None and not self.monitor_info: + return + elif monitor is not None: + self.monitor_info = win32api.GetMonitorInfo(monitor) + params = ctypes.cast(msg.lParam, + ctypes.POINTER(NCCALCSIZE_PARAMS)).contents + params.rgrc[0].left = self.monitor_info['Work'][0] + params.rgrc[0].top = self.monitor_info['Work'][1] + params.rgrc[0].right = self.monitor_info['Work'][2] + params.rgrc[0].bottom = self.monitor_info['Work'][3] + + @classmethod + def isWindowMaximized(cls, hwnd) -> bool: + windowPlacement = win32gui.GetWindowPlacement(hwnd) + if not windowPlacement: + return False + return windowPlacement[1] == win32con.SW_MAXIMIZE diff --git a/myapp/pyobjects/__init__.py b/myapp/pyobjects/__init__.py new file mode 100644 index 00000000..9f472b14 --- /dev/null +++ b/myapp/pyobjects/__init__.py @@ -0,0 +1,17 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from .example_singleton import SingletonPyObject diff --git a/myapp/pyobjects/example_singleton.py b/myapp/pyobjects/example_singleton.py new file mode 100644 index 00000000..faf184b8 --- /dev/null +++ b/myapp/pyobjects/example_singleton.py @@ -0,0 +1,32 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from PySide6.QtCore import QObject, Signal, Property +from PySide6.QtQml import QmlElement, QmlSingleton + +QML_IMPORT_NAME = "pyobjects" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +@QmlSingleton +class SingletonPyObject(QObject): + + def get_exposed_property(self) -> str: + return 'py property' + + exposed_property_changed = Signal(str) + exposed_property = Property(str, get_exposed_property, notify=exposed_property_changed) diff --git a/myapp/startup.py b/myapp/startup.py new file mode 100644 index 00000000..c7f18916 --- /dev/null +++ b/myapp/startup.py @@ -0,0 +1,72 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import os +import sys + + +class StartUp: + """Necessary steps for environment, Python and Qt""" + + @staticmethod + def configure_qt_application_data(): + from PySide6.QtCore import QCoreApplication + QCoreApplication.setApplicationName('my app name') + QCoreApplication.setOrganizationName('my org name') + QCoreApplication.setApplicationVersion('my app version') + + @staticmethod + def configure_environment_variables(): + # Qt expects 'qtquickcontrols2.conf' at root level, but the way we handle resources does not allow that. + # So we need to override the path here + os.environ['QT_QUICK_CONTROLS_CONF'] = ':/data/qtquickcontrols2.conf' + + @staticmethod + def import_resources(): + # noinspection PyUnresolvedReferences + import myapp.generated_resources + + @staticmethod + def import_bindings(): + # noinspection PyUnresolvedReferences + import myapp.pyobjects + + @staticmethod + def start_application(): + from myapp.application import MyApplication + app = MyApplication(sys.argv) + + app.set_window_icon() + app.set_up_signals() + app.set_up_imports() + app.set_up_window_event_filter() + app.start_engine() + app.set_up_window_effects() + app.verify() + + sys.exit(app.exec()) + + +def perform_startup(): + we = StartUp() + + we.configure_qt_application_data() + we.configure_environment_variables() + + we.import_resources() + we.import_bindings() + + we.start_application() diff --git a/qml/app/MyAppMainPage.qml b/qml/app/MyAppMainPage.qml new file mode 100644 index 00000000..8f3db87b --- /dev/null +++ b/qml/app/MyAppMainPage.qml @@ -0,0 +1,97 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import header +import pyobjects + + +Page { + id: root + + required property var appWindow + + anchors { + fill: root + } + + header: MyAppHeader { + appWindow: root.appWindow + } + + ColumnLayout { + spacing: 8 + width: root.width + + Image { + source: "qrc:/data/app-icon.svg" + asynchronous: true + + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 308 + Layout.preferredHeight: 226 + Layout.topMargin: 30 + } + + Label { + text: Qt.application.name + ' (' + Qt.application.version + ')' + + font { + bold: true + pixelSize: Qt.application.font.pixelSize * 1.5 + } + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 45 + } + + Label { + text: 'Running on ' + Qt.platform.os + + font { + bold: true + pixelSize: Qt.application.font.pixelSize * 1.5 + } + + Layout.alignment: Qt.AlignHCenter + } + + Label { + text: qsTranslate("MainPage", "Have fun!") + color: Material.accent + + font { + bold: true + pixelSize: Qt.application.font.pixelSize * 1.5 + } + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 45 + } + + Label { + text: qsTranslate("MainPage", "Exposed from Python: '%1'").arg(SingletonPyObject.exposed_property) + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 45 + } + } + +} diff --git a/qml/app/qmldir b/qml/app/qmldir new file mode 100644 index 00000000..4ba53f24 --- /dev/null +++ b/qml/app/qmldir @@ -0,0 +1,2 @@ +qmldir app +MyAppMainPage MyAppMainPage.qml diff --git a/qml/header/MyAppHeader.qml b/qml/header/MyAppHeader.qml new file mode 100644 index 00000000..3c3da662 --- /dev/null +++ b/qml/header/MyAppHeader.qml @@ -0,0 +1,60 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick + + +Item { + id: root + + required property var appWindow + + width: parent.width + height: headerBar.height + + TapHandler { + gesturePolicy: TapHandler.DragThreshold + + onTapped: { + if (tapCount === 2) { + if (root.appWindow.visibility === Window.Maximized) { + root.appWindow.showNormal() + } else { + root.appWindow.showMaximized() + } + } + } + } + + DragHandler { + target: null + grabPermissions: TapHandler.CanTakeOverFromAnything + + onActiveChanged: { + if (active) { + root.appWindow.startSystemMove() + } + } + } + + MyAppHeaderContent { + id: headerBar + + appWindow: root.appWindow + } + +} diff --git a/qml/header/MyAppHeaderContent.qml b/qml/header/MyAppHeaderContent.qml new file mode 100644 index 00000000..0676f4b6 --- /dev/null +++ b/qml/header/MyAppHeaderContent.qml @@ -0,0 +1,135 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick +import QtQuick.Controls + + +Item { + id: root + + required property var appWindow + + width: parent.width + height: menuBar.height + + Row { + width: root.width + spacing: 0 + + MenuBar { + id: menuBar + + background: Rectangle { + color: "transparent" + } + + MyAppMenu1 {} + MyAppMenu2 {} + MyAppOptionsMenu {} + MyAppHelpMenu {} + } + + Label { + text: Qt.application.name + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + width: root.width - menuBar.width * 2 + height: menuBar.height + elide: LayoutMirroring.enabled ? Text.ElideLeft : Text.ElideRight + } + + Item { + id: buttonWrapper + + width: menuBar.width + height: menuBar.height + + ToolButton { + objectName: 'minimizeButton' + + height: buttonWrapper.height + focusPolicy: Qt.NoFocus + + icon { + source: "qrc:/data/icons/minimize_black_24dp.svg" + width: 18 + height: 18 + } + + anchors { + right: maximizeButton.left + } + + onClicked: { + root.appWindow.showMinimized() + } + } + + ToolButton { + id: maximizeButton + + focusPolicy: Qt.NoFocus + height: buttonWrapper.height + + icon { + property bool maximized: root.appWindow.visibility === Window.Maximized + property url iconMaximize: "qrc:/data/icons/open_in_full_black_24dp.svg" + property url iconNormalize: "qrc:/data/icons/close_fullscreen_black_24dp.svg" + + source: maximized ? iconNormalize : iconMaximize + width: 18 + height: 18 + } + + anchors { + right: closeButton.left + } + + onClicked: { + if (root.appWindow.visibility === Window.Maximized) { + root.appWindow.showNormal() + } else { + root.appWindow.showMaximized() + } + } + } + + ToolButton { + id: closeButton + + height: buttonWrapper.height + focusPolicy: Qt.NoFocus + + icon { + source: "qrc:/data/icons/close_black_24dp.svg" + width: 18 + height: 18 + } + + anchors { + right: buttonWrapper.right + } + + onClicked: { + root.appWindow.close() + } + } + } + } + +} diff --git a/qml/header/MyAppHelpMenu.qml b/qml/header/MyAppHelpMenu.qml new file mode 100644 index 00000000..69593e81 --- /dev/null +++ b/qml/header/MyAppHelpMenu.qml @@ -0,0 +1,53 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick.Controls + +import shared + + +MyAppAutoWidthMenu { + title: qsTranslate("HeaderBar", "&Help") + + Action { + text: qsTranslate("HeaderBar", "&Action 1") + shortcut: "CTRL+N" + + onTriggered: { + console.log("Action 1 pressed") + } + } + + Action { + text: qsTranslate("HeaderBar", "&Action 2") + + onTriggered: { + console.log("Action 2 pressed") + } + } + + MenuSeparator { } + + Action { + text: qsTranslate("HeaderBar", "&Action 3") + + onTriggered: { + console.log("Action 3 pressed") + } + } + +} diff --git a/qml/header/MyAppMenu1.qml b/qml/header/MyAppMenu1.qml new file mode 100644 index 00000000..fea7761a --- /dev/null +++ b/qml/header/MyAppMenu1.qml @@ -0,0 +1,69 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick.Controls + +import shared + + +MyAppAutoWidthMenu { + title: qsTranslate("HeaderBar", "&Menu 1") + + Action { + text: qsTranslate("HeaderBar", "&Action 1") + shortcut: "CTRL+N" + + onTriggered: { + console.log("Action 1 pressed") + } + } + + Action { + text: qsTranslate("HeaderBar", "&Action 2") + + onTriggered: { + console.log("Action 2 pressed") + } + } + + Action { + text: qsTranslate("HeaderBar", "&Action 3") + + onTriggered: { + console.log("Action 3 pressed") + } + } + + Action { + text: qsTranslate("HeaderBar", "&Action 4") + + onTriggered: { + console.log("Action 4 pressed") + } + } + + MenuSeparator { } + + Action { + text: qsTranslate("HeaderBar", "&Action 5") + + onTriggered: { + console.log("Action 5 pressed") + } + } + +} diff --git a/qml/header/MyAppMenu2.qml b/qml/header/MyAppMenu2.qml new file mode 100644 index 00000000..556fdb6e --- /dev/null +++ b/qml/header/MyAppMenu2.qml @@ -0,0 +1,53 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick.Controls + +import shared + + +MyAppAutoWidthMenu { + title: qsTranslate("HeaderBar", "&Menu 2") + + Action { + text: qsTranslate("HeaderBar", "&Action 1") + shortcut: "CTRL+N" + + onTriggered: { + console.log("Action 1 pressed") + } + } + + Action { + text: qsTranslate("HeaderBar", "&Action 2") + + onTriggered: { + console.log("Action 2 pressed") + } + } + + MenuSeparator { } + + Action { + text: qsTranslate("HeaderBar", "&Action 3") + + onTriggered: { + console.log("Action 3 pressed") + } + } + +} diff --git a/qml/header/MyAppOptionsMenu.qml b/qml/header/MyAppOptionsMenu.qml new file mode 100644 index 00000000..e123cb03 --- /dev/null +++ b/qml/header/MyAppOptionsMenu.qml @@ -0,0 +1,85 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import shared +import models + + +MyAppAutoWidthMenu { + id: root + + title: qsTranslate("HeaderBar", "&Options") + + MyAppAutoWidthMenu { + title: qsTranslate("HeaderBar", "&Language") + + Repeater { + model: MyAppLanguageModel {} + + MenuItem { + id: _itemDelegate + + required property string language // from model + required property string abbrev // from model + + text: qsTranslate("Languages", _itemDelegate.language) + + onTriggered: { + animationDelayTimer.start() + } + + Timer { + id: animationDelayTimer + + interval: 125 + + onTriggered: { + Qt.uiLanguage = _itemDelegate.abbrev + } + } + } + } + } + + MenuSeparator { + } + + Action { + text: qsTranslate("HeaderBar", "Showcase translated Qt internal strings") + + property var factory: Component + { + MessageDialog { + title: qsTranslate("MessageBoxes", "Title") + text: qsTranslate("MessageBoxes", "Change the language and look at the 'Yes' and 'Cancel' buttons") + buttons: MessageDialog.Yes | MessageDialog.Cancel + visible: true + } + } + + onTriggered: { + const dialog = factory.createObject(root) + dialog.closed.connect(dialog.destroy) + dialog.open() + } + } + +} diff --git a/qml/header/qmldir b/qml/header/qmldir new file mode 100644 index 00000000..59c24db6 --- /dev/null +++ b/qml/header/qmldir @@ -0,0 +1,2 @@ +module header +MyAppHeader MyAppHeader.qml diff --git a/qml/main.qml b/qml/main.qml new file mode 100644 index 00000000..12152019 --- /dev/null +++ b/qml/main.qml @@ -0,0 +1,83 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick +import QtQuick.Controls + +import app + + +ApplicationWindow { + id: root + + width: 1280 + height: 720 + flags: Qt.FramelessWindowHint | Qt.Window + visible: true + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + MyAppMainPage { + appWindow: _shared + + anchors { + fill: root.contentItem + margins: _private.windowBorder + } + } + + Component.onCompleted: { + // load language from settings + // Qt.uiLanguage = ... + } + + QtObject { + id: _private // Implementation details not exposed to child items + + readonly property bool maximized: root.visibility === Window.Maximized + readonly property bool fullscreen: root.visibility === Window.FullScreen + readonly property int windowBorder: fullscreen || maximized ? 0 : 1 + } + + QtObject { + id: _shared // Properties and functions exposed to child items + + readonly property var visibility: root.visibility + + function startSystemMove() { + root.startSystemMove() + } + + function showMinimized() { + root.showMinimized() + } + + function showMaximized() { + root.showMaximized() + } + + function showNormal() { + root.showNormal() + } + + function close() { + root.close() + } + } + +} diff --git a/qml/models/MyAppLanguageModel.qml b/qml/models/MyAppLanguageModel.qml new file mode 100644 index 00000000..0dcd35ee --- /dev/null +++ b/qml/models/MyAppLanguageModel.qml @@ -0,0 +1,40 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick + + +ListModel { + readonly property var languagesForTranslationTool: [ + qsTranslate("Languages", "English"), + qsTranslate("Languages", "German"), + qsTranslate("Languages", "Hebrew"), + ] + + ListElement { + language: "English" + abbrev: "en_US" + } + ListElement { + language: "German" + abbrev: "de_DE" + } + ListElement { + language: "Hebrew" + abbrev: "he_IL" + } +} diff --git a/qml/models/qmldir b/qml/models/qmldir new file mode 100644 index 00000000..506dc45e --- /dev/null +++ b/qml/models/qmldir @@ -0,0 +1,2 @@ +module models +MyAppLanguageModel MyAppLanguageModel.qml diff --git a/qml/models/tst_MyAppLanguageModel.qml b/qml/models/tst_MyAppLanguageModel.qml new file mode 100644 index 00000000..75e2c791 --- /dev/null +++ b/qml/models/tst_MyAppLanguageModel.qml @@ -0,0 +1,65 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick +import QtTest + + +TestCase { + id: testCase + + name: "MyAppLanguageModelTest" + + Component { + id: objectUnderTest + + MyAppLanguageModel {} + } + + function extractLanguagesFrom(model: MyAppLanguageModel): Array { + const languages = [] + for (let i = 0; i < model.count; i++) { + languages.push(model.get(i).abbrev) + } + return languages + } + + function test_languageExists_data() { + return [ + {tag: 'de_DE', abbrev: 'de_DE'}, + {tag: 'en_US', abbrev: 'en_US'}, + {tag: 'he_IL', abbrev: 'he_IL'}, + ] + } + + function test_languageExists(data) { + const control = createTemporaryObject(objectUnderTest, testCase) + verify(control) + + const languages = extractLanguagesFrom(control) + verify(languages.includes(data.abbrev)) + } + + function test_languageDoesNotExist() { + const control = createTemporaryObject(objectUnderTest, testCase) + verify(control) + + const languages = extractLanguagesFrom(control) + verify(!languages.includes('something-else')) + } + +} diff --git a/qml/shared/MyAppAutoWidthMenu.qml b/qml/shared/MyAppAutoWidthMenu.qml new file mode 100644 index 00000000..bb227319 --- /dev/null +++ b/qml/shared/MyAppAutoWidthMenu.qml @@ -0,0 +1,52 @@ +/* +Copyright + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import QtQuick +import QtQuick.Controls + + +Menu { + id: root + + /* + Taken and adapted from: + https://martin.rpdev.net/2018/03/13/qt-quick-controls-2-automatically-set-the-width-of-menus.html + */ + + readonly property bool mMirrored: count > 0 && itemAt(0).mirrored + + x: mMirrored ? -width + parent.width : 0 + + width: { + let result = 0 + let padding = 0 + for (let i = 0; i < root.count; ++i) { + let item = root.itemAt(i) + + if (!isMenuSeparator(item)) { + result = Math.max(item.contentItem.implicitWidth, result) + padding = Math.max(item.padding, padding) + } + } + return result + padding * 2 + } + + function isMenuSeparator(item) { + return item instanceof MenuSeparator + } + +} diff --git a/qml/shared/qmldir b/qml/shared/qmldir new file mode 100644 index 00000000..35b5150f --- /dev/null +++ b/qml/shared/qmldir @@ -0,0 +1,2 @@ +module components.shared +MyAppAutoWidthMenu MyAppAutoWidthMenu.qml diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ffb7031f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PySide6-Essentials==6.8.0 +pytest>=8.3.3 +pywin32>=307; sys_platform == 'win32' diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..f8c365e8 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,23 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +try: + import test.generated_resources +except ImportError: + import sys + + print('Can not find resource module \'test.generated_resources\'', file=sys.stderr) + print('To execute individual tests, please run \'just test\' once before', file=sys.stderr) + sys.exit(1) diff --git a/test/services/__init__.py b/test/services/__init__.py new file mode 100644 index 00000000..b88e35f5 --- /dev/null +++ b/test/services/__init__.py @@ -0,0 +1,13 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/test/services/test_resource_availability.py b/test/services/test_resource_availability.py new file mode 100644 index 00000000..ae44f418 --- /dev/null +++ b/test/services/test_resource_availability.py @@ -0,0 +1,33 @@ +# Copyright +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import pytest +from PySide6.QtCore import QFile + + +@pytest.mark.parametrize('file_path', [ + ':/data/app-icon.svg', + ':/i18n/de_DE.qm', + ':/i18n/he_IL.qm', +]) +def test_resource_exist(file_path): + file = QFile(file_path) + assert file.exists() + + +def test_resource_does_not_exist(): + file = QFile(':/random/file/which/not.exists') + assert not file.exists()