├── .github └── workflows │ ├── python-publish-to-pypi.yml │ └── python-publish-to-test-pypi.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── pyside6_utils ├── __init__.py ├── classes │ ├── __init__.py │ ├── constraints.py │ └── serializable.py ├── constants.py ├── examples │ ├── __init__.py │ ├── example_dataclass.py │ ├── images │ │ ├── Qt_designer_loaded_widgets_example.png │ │ ├── collapsible_group_box_collapsed.png │ │ ├── collapsible_group_box_open.png │ │ ├── console_widget.png │ │ ├── dataclass_view.png │ │ ├── extended_mdi_area.png │ │ ├── file_explorer_view.png │ │ ├── overlay_widget.png │ │ ├── pandas_table_view.png │ │ ├── range_selector.png │ │ ├── square_widget.png │ │ ├── string_float_list_example.png │ │ ├── widget_list.png │ │ └── widget_switcher.png │ ├── run_qt_designer.py │ └── run_widgets_example_app.py ├── icons │ ├── __init__.py │ ├── actions │ │ ├── address-book-new.png │ │ ├── appointment-new.png │ │ ├── bookmark-new.png │ │ ├── contact-new.png │ │ ├── document-new.png │ │ ├── document-open.png │ │ ├── document-print-preview.png │ │ ├── document-print.png │ │ ├── document-properties.png │ │ ├── document-save-as.png │ │ ├── document-save.png │ │ ├── edit-clear.png │ │ ├── edit-copy.png │ │ ├── edit-cut.png │ │ ├── edit-delete.png │ │ ├── edit-find-replace.png │ │ ├── edit-find.png │ │ ├── edit-paste.png │ │ ├── edit-redo.png │ │ ├── edit-select-all.png │ │ ├── edit-undo.png │ │ ├── folder-new.png │ │ ├── format-indent-less.png │ │ ├── format-indent-more.png │ │ ├── format-justify-center.png │ │ ├── format-justify-fill.png │ │ ├── format-justify-left.png │ │ ├── format-justify-right.png │ │ ├── format-text-bold.png │ │ ├── format-text-italic.png │ │ ├── format-text-strikethrough.png │ │ ├── format-text-underline.png │ │ ├── go-bottom.png │ │ ├── go-down.png │ │ ├── go-first.png │ │ ├── go-home.png │ │ ├── go-jump.png │ │ ├── go-last.png │ │ ├── go-next.png │ │ ├── go-previous.png │ │ ├── go-top.png │ │ ├── go-up.png │ │ ├── list-add.png │ │ ├── list-remove.png │ │ ├── mail-forward.png │ │ ├── mail-mark-junk.png │ │ ├── mail-mark-not-junk.png │ │ ├── mail-message-new.png │ │ ├── mail-reply-all.png │ │ ├── mail-reply-sender.png │ │ ├── mail-send-receive.png │ │ ├── media-eject.png │ │ ├── media-playback-pause.png │ │ ├── media-playback-start.png │ │ ├── media-playback-stop.png │ │ ├── media-record.png │ │ ├── media-seek-backward.png │ │ ├── media-seek-forward.png │ │ ├── media-skip-backward.png │ │ ├── media-skip-forward.png │ │ ├── process-stop.png │ │ ├── system-lock-screen.png │ │ ├── system-log-out.png │ │ ├── system-search.png │ │ ├── system-shutdown.png │ │ ├── tab-new.png │ │ ├── view-fullscreen.png │ │ ├── view-refresh.png │ │ └── window-new.png │ ├── app_resources.qrc │ ├── app_resources_rc.py │ ├── apps │ │ ├── accessories-calculator.png │ │ ├── accessories-character-map.png │ │ ├── accessories-text-editor.png │ │ ├── help-browser.png │ │ ├── internet-group-chat.png │ │ ├── internet-mail.png │ │ ├── internet-news-reader.png │ │ ├── internet-web-browser.png │ │ ├── office-calendar.png │ │ ├── preferences-desktop-accessibility.png │ │ ├── preferences-desktop-assistive-technology.png │ │ ├── preferences-desktop-font.png │ │ ├── preferences-desktop-keyboard-shortcuts.png │ │ ├── preferences-desktop-locale.png │ │ ├── preferences-desktop-multimedia.png │ │ ├── preferences-desktop-remote-desktop.png │ │ ├── preferences-desktop-screensaver.png │ │ ├── preferences-desktop-theme.png │ │ ├── preferences-desktop-wallpaper.png │ │ ├── preferences-system-network-proxy.png │ │ ├── preferences-system-session.png │ │ ├── preferences-system-windows.png │ │ ├── system-file-manager.png │ │ ├── system-installer.png │ │ ├── system-software-update.png │ │ ├── system-users.png │ │ ├── utilities-system-monitor.png │ │ └── utilities-terminal.png │ ├── categories │ │ ├── applications-accessories.png │ │ ├── applications-development.png │ │ ├── applications-games.png │ │ ├── applications-graphics.png │ │ ├── applications-internet.png │ │ ├── applications-multimedia.png │ │ ├── applications-office.png │ │ ├── applications-other.png │ │ ├── applications-system.png │ │ ├── preferences-desktop-peripherals.png │ │ ├── preferences-desktop.png │ │ └── preferences-system.png │ ├── devices │ │ ├── audio-card.png │ │ ├── audio-input-microphone.png │ │ ├── battery.png │ │ ├── camera-photo.png │ │ ├── camera-video.png │ │ ├── computer.png │ │ ├── drive-harddisk.png │ │ ├── drive-optical.png │ │ ├── drive-removable-media.png │ │ ├── input-gaming.png │ │ ├── input-keyboard.png │ │ ├── input-mouse.png │ │ ├── media-flash.png │ │ ├── media-floppy.png │ │ ├── media-optical.png │ │ ├── multimedia-player.png │ │ ├── network-wired.png │ │ ├── network-wireless.png │ │ ├── printer.png │ │ └── video-display.png │ ├── emblems │ │ ├── emblem-favorite.png │ │ ├── emblem-important.png │ │ ├── emblem-photos.png │ │ ├── emblem-readonly.png │ │ ├── emblem-symbolic-link.png │ │ ├── emblem-system.png │ │ └── emblem-unreadable.png │ ├── emotes │ │ ├── face-angel.png │ │ ├── face-crying.png │ │ ├── face-devilish.png │ │ ├── face-glasses.png │ │ ├── face-grin.png │ │ ├── face-kiss.png │ │ ├── face-monkey.png │ │ ├── face-plain.png │ │ ├── face-sad.png │ │ ├── face-smile-big.png │ │ ├── face-smile.png │ │ ├── face-surprise.png │ │ └── face-wink.png │ ├── mimetypes │ │ ├── application-certificate.png │ │ ├── application-x-executable.png │ │ ├── audio-x-generic.png │ │ ├── font-x-generic.png │ │ ├── image-x-generic.png │ │ ├── package-x-generic.png │ │ ├── text-html.png │ │ ├── text-x-generic-template.png │ │ ├── text-x-generic.png │ │ ├── text-x-script.png │ │ ├── video-x-generic.png │ │ ├── x-office-address-book.png │ │ ├── x-office-calendar.png │ │ ├── x-office-document-template.png │ │ ├── x-office-document.png │ │ ├── x-office-drawing-template.png │ │ ├── x-office-drawing.png │ │ ├── x-office-presentation-template.png │ │ ├── x-office-presentation.png │ │ ├── x-office-spreadsheet-template.png │ │ └── x-office-spreadsheet.png │ ├── places │ │ ├── folder-remote.png │ │ ├── folder-saved-search.png │ │ ├── folder.png │ │ ├── network-server.png │ │ ├── network-workgroup.png │ │ ├── start-here.png │ │ ├── user-desktop.png │ │ ├── user-home.png │ │ └── user-trash.png │ └── status │ │ ├── audio-volume-high.png │ │ ├── audio-volume-low.png │ │ ├── audio-volume-medium.png │ │ ├── audio-volume-muted.png │ │ ├── battery-caution.png │ │ ├── dialog-error.png │ │ ├── dialog-information.png │ │ ├── dialog-warning.png │ │ ├── folder-drag-accept.png │ │ ├── folder-open.png │ │ ├── folder-visiting.png │ │ ├── image-loading.png │ │ ├── image-missing.png │ │ ├── mail-attachment.png │ │ ├── network-error.png │ │ ├── network-idle.png │ │ ├── network-offline.png │ │ ├── network-receive.png │ │ ├── network-transmit-receive.png │ │ ├── network-transmit.png │ │ ├── network-wireless-encrypted.png │ │ ├── printer-error.png │ │ ├── software-update-available.png │ │ ├── software-update-urgent.png │ │ ├── user-trash-full.png │ │ ├── weather-clear-night.png │ │ ├── weather-clear.png │ │ ├── weather-few-clouds-night.png │ │ ├── weather-few-clouds.png │ │ ├── weather-overcast.png │ │ ├── weather-severe-alert.png │ │ ├── weather-showers-scattered.png │ │ ├── weather-showers.png │ │ ├── weather-snow.png │ │ └── weather-storm.png ├── models │ ├── __init__.py │ ├── console_widget_models │ │ ├── __init__.py │ │ ├── console_from_file_item.py │ │ └── console_model.py │ ├── dataclass_model.py │ ├── dataclass_tree_item.py │ ├── extended_sort_filter_proxy_model.py │ ├── file_explorer_model.py │ └── pandas_table_model.py ├── registrars │ ├── __init__.py │ ├── register_collapsible_group_box.py │ ├── register_console_widget.py │ ├── register_extended_mdi_area.py │ ├── register_file_explorer_view.py │ ├── register_overlay_widget.py │ ├── register_pandas_table.py │ ├── register_range_selector.py │ ├── register_square_frame.py │ ├── register_widget_list.py │ └── register_widget_switcher.py ├── ui │ ├── AllWidgets.ui │ ├── ConsoleWidget.ui │ ├── ConsoleWidget_ui.py │ ├── FramelessMdiWindow.ui │ ├── FramelessMdiWindow_ui.py │ ├── __init__.py │ └── allWidgets_ui.py ├── utility │ ├── __init__.py │ ├── catch_show_exception_in_popup_decorator.py │ ├── signal_blocker.py │ └── utility_functions.py └── widgets │ ├── __init__.py │ ├── collapsible_group_box.py │ ├── console_widget.py │ ├── dataclass_tree_view.py │ ├── delegates │ ├── __init__.py │ ├── console_widget_delegate.py │ └── dataclass_editors_delegate.py │ ├── extended_mdi_area.py │ ├── file_explorer_view.py │ ├── frameless_mdi_window.py │ ├── overlay_widget.py │ ├── pandas_table_view.py │ ├── range_selector.py │ ├── square_frame.py │ ├── widget_list.py │ └── widget_switcher.py ├── requirements.txt └── setup.py /.github/workflows/python-publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package to mypi.org 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: #Uses trusted publishing (https://github.com/pypa/gh-action-pypi-publish) - make sure publisher is added in pypi-project 20 | runs-on: ubuntu-latest 21 | environment: 22 | name: pypi 23 | url: https://pypi.org/p/pyside6-utils 24 | permissions: 25 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Python 30 | uses: actions/setup-python@v3 31 | with: 32 | python-version: '3.x' 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install build 37 | - name: Build package 38 | run: python -m build 39 | - name: Publish the built release/package distribution to test.pypi.org 40 | uses: pypa/gh-action-pypi-publish@release/v1 41 | -------------------------------------------------------------------------------- /.github/workflows/python-publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package to test.mypi.org 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: #Uses trusted publishing (https://github.com/pypa/gh-action-pypi-publish) - make sure publisher is added in pypi-project 20 | runs-on: ubuntu-latest 21 | environment: 22 | name: pypi 23 | url: https://pypi.org/p/pyside6-utils 24 | permissions: 25 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Python 30 | uses: actions/setup-python@v3 31 | with: 32 | python-version: '3.x' 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install build 37 | - name: Build package 38 | run: python -m build 39 | - name: Publish the built release/package distribution to test.pypi.org 40 | uses: pypa/gh-action-pypi-publish@release/v1 41 | with: 42 | repository-url: https://test.pypi.org/legacy/ 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.code-workspace 3 | __pycache__/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE -------------------------------------------------------------------------------- /pyside6_utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Make all submodule available""" 2 | -------------------------------------------------------------------------------- /pyside6_utils/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from .serializable import Serializable -------------------------------------------------------------------------------- /pyside6_utils/classes/serializable.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper class to serialize and deserialize objects 3 | for now, only json is supported, but more formats can be added. 4 | """ 5 | import json 6 | 7 | class Serializable(): 8 | """Generic class to serialize and deserialize objects to/from files""" 9 | def to_file(self, path : str, filetype = "json", ignore_private = False, use_encoding="utf-8"): 10 | """Trioes to find the correct function to save this object to a file, then saves it to the passed path 11 | 12 | Args: 13 | path (str): The path to save the file to 14 | filetype (str, optional): What filetype to save to. Defaults to "json". 15 | ignore_private (bool, optional): Whether to ignore private attributes. Defaults to True. 16 | use_encoding (str, optional): What encoding to use. Defaults to "utf-8". 17 | 18 | Raises: 19 | NotImplementedError: _description_ 20 | """ 21 | func = getattr(self, "to_"+filetype) 22 | if func is None: 23 | raise NotImplementedError(f"Could not save to passed filetype ({filetype}), this filetype is not implemented") 24 | else: 25 | output = func(ignore_private=ignore_private) 26 | with open(path, "w", encoding=use_encoding) as file: 27 | file.write(output) 28 | 29 | def from_file(self, path : str, filetype = "json", use_encoding="utf-8"): 30 | """ 31 | Load this class from a file 32 | Args: 33 | path (str): The path to load the file from 34 | filetype (str, optional): What filetype to load from. Defaults to "json". 35 | use_encoding (str, optional): What encoding to use - if applicable. Defaults to "utf-8". 36 | """ 37 | # if filetype == "json": 38 | # with open(path, "w") as file: 39 | # file.write(self.tojson()) 40 | 41 | func = getattr(self, "from_"+filetype) 42 | 43 | if func is None or filetype == "to_": 44 | raise NotImplementedError(f"Could not save to passed filetype ({filetype}), this filetype is not implemented") 45 | else: 46 | with open(path, "r", encoding=use_encoding) as file: 47 | func(file.read()) 48 | 49 | # self.copy_from_dict(dict) 50 | 51 | 52 | def copy_from_dict(self, copy_from_dict : dict, ignore_new_attributes = False): 53 | """ Copies all attributes from a dict to this object 54 | 55 | Args: 56 | copy_from_dict (dict): Dictionary to copy from 57 | ignore_new_attributes (bool, optional): If True, will ignore attributes that are not already present in this 58 | object. Defaults to False. 59 | 60 | Returns: 61 | list: list of items that were in the copy dict, but no attribute of this object 62 | list: list of items that were in this item, but not in the copy dict 63 | """ 64 | problem_list = set([]) 65 | if ignore_new_attributes: 66 | for key in copy_from_dict: 67 | if not hasattr(self, key): 68 | problem_list.add(key) 69 | 70 | for key in copy_from_dict: 71 | if ignore_new_attributes and key in problem_list: 72 | continue 73 | setattr(self, key, copy_from_dict[key]) 74 | 75 | missing_keys = set([key for key in self.__dict__ if key not in copy_from_dict]) 76 | 77 | return list(problem_list), list(missing_keys) #Return list of attributes that were not set 78 | 79 | 80 | def to_json(self, ignore_private = False): 81 | """Returns a json string representation of this object when converted to a dict""" 82 | if ignore_private: 83 | raise NotImplementedError("Ignore-private json conversion not implemented yet") 84 | return json.dumps(self, default=lambda o: o.__dict__, 85 | sort_keys=True, indent=4) 86 | 87 | def from_json(self, json_string): 88 | """Loads this object from a json string - all attributes present in the json string will be set on this object""" 89 | load_dict = json.loads(json_string) 90 | for key in load_dict: 91 | setattr(self, key, load_dict[key]) #Try to set all attributes from json 92 | -------------------------------------------------------------------------------- /pyside6_utils/constants.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Defines some constants used in the package. 4 | """ 5 | from enum import Enum 6 | 7 | class Paths(str, Enum): 8 | """Paths to the package's subfolders. Used for the registrars""" 9 | PACKAGE_NAME = "pyside6_utils" 10 | WIDGETS_SUBPATH = "widgets" 11 | MODELS_SUBPATH = "models" 12 | EXAMPLES_SUBPATH = "examples" 13 | -------------------------------------------------------------------------------- /pyside6_utils/examples/__init__.py: -------------------------------------------------------------------------------- 1 | """Make it so we can directly import the examples.""" 2 | 3 | # from .run_widgets_example_app import run_widgets_example_app #Work in progres... 4 | from .run_qt_designer import run_qt_designer 5 | from .example_dataclass import ExampleDataClass -------------------------------------------------------------------------------- /pyside6_utils/examples/example_dataclass.py: -------------------------------------------------------------------------------- 1 | """An example dataclass to show functionality of automatic GUI generation.""" 2 | 3 | import typing 4 | from dataclasses import dataclass, field 5 | from datetime import datetime 6 | 7 | from pyside6_utils.classes.constraints import (Interval, ConstrainedList, 8 | StrOptions) 9 | 10 | LITERAL_EXAMPLE = typing.Literal["testliteral1", "testliteral2", "testliteral3"] #pylint: disable=invalid-name 11 | 12 | @dataclass 13 | class ExampleDataClass: 14 | """ 15 | This is an example dataclass to test automatic GUI generation - shows the functionality of all types. 16 | """ 17 | 18 | test_parent_with_property: str = field( 19 | default="testparentwithpropertystr", 20 | metadata=dict( 21 | display_name="Test Parent With Value", 22 | help= "This is a parent with an unsettable property", 23 | # display_path="Test Parent With Value", #Always from base 24 | editable=False 25 | ) 26 | ) 27 | 28 | test_str: str = field( 29 | default="teststr", 30 | metadata=dict( 31 | display_name="Test str", 32 | help= "This is a test", 33 | display_path="test_parent_with_property/Dummy Parent", #Always from base 34 | ) 35 | ) 36 | 37 | test_int: int | None = field( 38 | default=None, 39 | metadata=dict( 40 | display_name="Test int/none", 41 | help= "This is a test that can also be none", 42 | changed=True 43 | ) 44 | ) 45 | 46 | test_literal: typing.Literal["testliteral1", "testliteral2", "testliteral3"] = field( 47 | default="testliteral1", 48 | metadata=dict( 49 | display_name="Test literal", 50 | help= "This is a test", 51 | changed=True 52 | ) 53 | ) 54 | 55 | test_required_int: int | None = field( 56 | default=None, 57 | metadata=dict( 58 | display_name="Test required int", 59 | help= "This is a test int that is required", 60 | changed=True, 61 | required=True 62 | ) 63 | ) 64 | 65 | test_int_literal : typing.Literal[1, 2, 3] = field( 66 | default=1, 67 | metadata=dict( 68 | display_name="Test int literal", 69 | help= "This is a test", 70 | changed=True 71 | ) 72 | ) 73 | 74 | test_int_options : int = field( 75 | default=1, 76 | metadata=dict( 77 | display_name="Test int options", 78 | help= "This is a test that stays within 0-10 interval", 79 | changed=True, 80 | constraints = [Interval(int, 0,10, closed='both')] 81 | ) 82 | ) 83 | 84 | test_intlist : typing.List[int] = field( 85 | default_factory=list, 86 | metadata=dict( 87 | display_name="Test intlist", 88 | help= "This is a test", 89 | changed=True, 90 | constraints = [ConstrainedList([Interval(int, 0,10, closed='both')]), None] 91 | ) 92 | ) 93 | 94 | test_int_or_none_list : typing.List[int | None] = field( 95 | default_factory=list, 96 | metadata=dict( 97 | display_name="Test int 0<->10 or None list", 98 | help= "This is a test", 99 | changed=True, 100 | constraints = [ 101 | ConstrainedList([Interval(int, 0,10, closed='both'), None]) 102 | ] 103 | ) 104 | ) 105 | 106 | 107 | test_float_or_option : typing.List[float | str] = field( 108 | default_factory=list, 109 | metadata=dict( 110 | display_name="Either 0-1 float or 'string1' or 'string2'", 111 | help= "This is a test", 112 | changed=True, 113 | constraints = [ 114 | ConstrainedList([Interval(float, 0,1, closed='both'), StrOptions({"string1", "string2"})]) 115 | ] 116 | ) 117 | ) 118 | 119 | test_float_or_int_list : typing.List[float | int] = field( 120 | default_factory=list, 121 | metadata=dict( 122 | display_name="Test List[float|int]", 123 | help= "This is a test using only typing.List[float|int], no constraints", 124 | changed=True, 125 | ) 126 | ) 127 | 128 | test_stroptions_property : typing.Literal["Option 1/10", "Option 2/10"] = field( 129 | default="Option 1/10", 130 | metadata=dict( 131 | display_name="Test stroptions", 132 | help= "This is a test (stroptions)", 133 | changed=True, 134 | constraints = [StrOptions({f"Option {i}/10" for i in range(10)}), None], #Similar to literal, but we can 135 | #create additional options dynamically using constraints 136 | constraints_help= { f"Option {i}/10" : f"This is help for option {i}/10" for i in range(10) } #This option 137 | #allows us to add a "help" popup when hovering over individual options in a combobox 138 | ) 139 | ) 140 | 141 | 142 | test_float_range_0_1_property: float = field( 143 | default=0.001, 144 | metadata=dict( 145 | display_name="Test float range 0-1", 146 | help= "This is a test float", 147 | changed=True, 148 | constraints = [Interval(float, 0,1, closed='both'), None] 149 | ) 150 | ) 151 | 152 | test_bool_property: bool = field( 153 | default=False, 154 | metadata=dict( 155 | display_name="Test bool", 156 | help= "This is a test bool", 157 | changed=True 158 | ) 159 | ) 160 | 161 | test_datetime_property: datetime = field( 162 | default=datetime(2050,1,1), 163 | metadata=dict( 164 | display_name="Test datetime", 165 | help= "This is a test datetime", 166 | changed=True 167 | ) 168 | ) 169 | -------------------------------------------------------------------------------- /pyside6_utils/examples/images/Qt_designer_loaded_widgets_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/Qt_designer_loaded_widgets_example.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/collapsible_group_box_collapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/collapsible_group_box_collapsed.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/collapsible_group_box_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/collapsible_group_box_open.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/console_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/console_widget.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/dataclass_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/dataclass_view.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/extended_mdi_area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/extended_mdi_area.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/file_explorer_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/file_explorer_view.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/overlay_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/overlay_widget.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/pandas_table_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/pandas_table_view.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/range_selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/range_selector.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/square_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/square_widget.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/string_float_list_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/string_float_list_example.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/widget_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/widget_list.png -------------------------------------------------------------------------------- /pyside6_utils/examples/images/widget_switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/examples/images/widget_switcher.png -------------------------------------------------------------------------------- /pyside6_utils/examples/run_qt_designer.py: -------------------------------------------------------------------------------- 1 | """ 2 | 2023 3 | This example will run QT Designer, also setting the PYSIDE_DESIGNER_PLUGINS path to the appropriate location, 4 | which should make all widgets in this package available in QT Designer. 5 | """ 6 | import os 7 | import pathlib 8 | import sys 9 | 10 | from PySide6.QtCore import QProcess, QProcessEnvironment 11 | from PySide6.QtWidgets import QApplication, QMessageBox 12 | 13 | 14 | def run_qt_designer(): 15 | """Runs qt designer, first setting the path to all registrars such that all custom widgets are loaded""" 16 | #Get path 1 folder up from this file 17 | main_path = pathlib.Path(__file__).parent.parent.absolute() 18 | env = QProcessEnvironment.systemEnvironment() 19 | env.insert('PYSIDE_DESIGNER_PLUGINS', os.path.join(main_path, "registrars")) 20 | 21 | 22 | app = QApplication(sys.argv) #pylint: disable=unused-variable 23 | QMessageBox.information( 24 | None, #type: ignore 25 | "PySide6 Designer", 26 | f"""

This example will attempt to run Qt Designer, including the PySide6 widgets.

27 |

After clicking OK, Qt Designer should be started.

28 |

This example assumes that you are using the right python environment in which PySide6 is installed. 29 | This means that Qt Designer can be launched by running: pyside6-designer - 30 | if not, this example will not work.

31 |

This script automatically sets the PYSIDE_DESIGNER_PLUGINS environment variable to ./Registrars 32 | so that all of the widgets in this repository should appear in the widget box in the PySide2 Widgets 33 | group-box.

34 | 35 |

Currently looking for Widgets/Registrars using path: {main_path}

36 | """ 37 | ) 38 | 39 | designer_process = QProcess() 40 | designer_process.setProcessEnvironment(env) 41 | designer_process.setProcessChannelMode(QProcess.ProcessChannelMode.ForwardedChannels) #Show output in console 42 | designer_process.start("pyside6-designer", [os.path.join(main_path, "ui", "AllWidgets.ui")]) #Start Qt Designer, 43 | #opening the example UI file 44 | 45 | #Check if designer is running 46 | if not designer_process.waitForStarted(): 47 | print("Designer process failed to start") 48 | #Create message box with reason why designer is not running 49 | QMessageBox.critical( 50 | None, #type: ignore 51 | "PySide6 Designer", 52 | f"

Qt Designer (pyside6-designer) could not be started. Please check that it is " 53 | f"installed and that the designer executable is in your " 54 | f"path.

" 55 | f"

The error message returned was:

" 56 | f"

{designer_process.errorString()}

" 57 | ) 58 | sys.exit(1) 59 | 60 | designer_process.waitForFinished(-1) #-1 otherwise we will quit after 30 seconds 61 | sys.exit(designer_process.exitCode()) 62 | 63 | 64 | 65 | if __name__ == "__main__": 66 | run_qt_designer() 67 | -------------------------------------------------------------------------------- /pyside6_utils/examples/run_widgets_example_app.py: -------------------------------------------------------------------------------- 1 | """Run a window with examples for the widgets in this package.""" 2 | 3 | import sys 4 | 5 | import pandas as pd 6 | from PySide6.QtWidgets import QApplication, QMainWindow 7 | 8 | from pyside6_utils.models.pandas_table_model import PandasTableModel 9 | from pyside6_utils.ui.allWidgets_ui import Ui_MainWindow 10 | 11 | 12 | def run_widgets_example_app(): 13 | """Creates a qt app and shows a window with all a couple of small widgets in this package. 14 | TODO: create buttons for the large widgets? 15 | """ 16 | app = QApplication(sys.argv) 17 | example_window = QMainWindow() 18 | example_ui = Ui_MainWindow() 19 | example_ui.setupUi(example_window) 20 | 21 | #====== Example df for PandasTableView ====== 22 | example_df = pd.DataFrame({ 23 | "Column 1": [1, 2, 3, 4, 5], 24 | "Column 2": [10, 20, 30, 40, 50], 25 | "Column 3": [100, 200, 300, 400, 500], 26 | "Column 4": [1000, 2000, 3000, 4000, 5000], 27 | "Column 5": [10000, 20000, 30000, 40000, 50000], 28 | }) 29 | example_df_model = PandasTableModel(example_df) 30 | example_ui.pandasTableView.setModel(example_df_model) 31 | example_ui.pandasTableView.set_status_bar(example_window.statusBar()) 32 | 33 | example_window.show() 34 | sys.exit(app.exec()) 35 | 36 | 37 | if __name__ == "__main__": 38 | run_widgets_example_app() 39 | -------------------------------------------------------------------------------- /pyside6_utils/icons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/__init__.py -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/address-book-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/address-book-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/appointment-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/appointment-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/bookmark-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/bookmark-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/contact-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/contact-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/document-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/document-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/document-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/document-open.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/document-print-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/document-print-preview.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/document-print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/document-print.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/document-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/document-properties.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/document-save-as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/document-save-as.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/document-save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/document-save.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-clear.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-copy.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-cut.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-delete.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-find-replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-find-replace.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-find.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-paste.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-redo.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-select-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-select-all.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/edit-undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/edit-undo.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/folder-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/folder-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-indent-less.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-indent-less.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-indent-more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-indent-more.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-justify-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-justify-center.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-justify-fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-justify-fill.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-justify-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-justify-left.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-justify-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-justify-right.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-text-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-text-bold.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-text-italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-text-italic.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-text-strikethrough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-text-strikethrough.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/format-text-underline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/format-text-underline.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-bottom.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-down.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-first.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-home.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-jump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-jump.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-last.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-next.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-previous.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-top.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/go-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/go-up.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/list-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/list-add.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/list-remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/list-remove.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/mail-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/mail-forward.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/mail-mark-junk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/mail-mark-junk.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/mail-mark-not-junk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/mail-mark-not-junk.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/mail-message-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/mail-message-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/mail-reply-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/mail-reply-all.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/mail-reply-sender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/mail-reply-sender.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/mail-send-receive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/mail-send-receive.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-eject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-eject.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-playback-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-playback-pause.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-playback-start.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-playback-stop.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-record.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-seek-backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-seek-backward.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-seek-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-seek-forward.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-skip-backward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-skip-backward.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/media-skip-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/media-skip-forward.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/process-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/process-stop.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/system-lock-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/system-lock-screen.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/system-log-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/system-log-out.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/system-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/system-search.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/system-shutdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/system-shutdown.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/tab-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/tab-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/view-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/view-fullscreen.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/view-refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/view-refresh.png -------------------------------------------------------------------------------- /pyside6_utils/icons/actions/window-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/actions/window-new.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/accessories-calculator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/accessories-calculator.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/accessories-character-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/accessories-character-map.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/accessories-text-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/accessories-text-editor.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/help-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/help-browser.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/internet-group-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/internet-group-chat.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/internet-mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/internet-mail.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/internet-news-reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/internet-news-reader.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/internet-web-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/internet-web-browser.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/office-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/office-calendar.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-accessibility.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-assistive-technology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-assistive-technology.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-font.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-keyboard-shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-keyboard-shortcuts.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-locale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-locale.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-multimedia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-multimedia.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-remote-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-remote-desktop.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-screensaver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-screensaver.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-theme.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-desktop-wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-desktop-wallpaper.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-system-network-proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-system-network-proxy.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-system-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-system-session.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/preferences-system-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/preferences-system-windows.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/system-file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/system-file-manager.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/system-installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/system-installer.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/system-software-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/system-software-update.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/system-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/system-users.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/utilities-system-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/utilities-system-monitor.png -------------------------------------------------------------------------------- /pyside6_utils/icons/apps/utilities-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/apps/utilities-terminal.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-accessories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-accessories.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-development.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-games.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-games.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-graphics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-graphics.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-internet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-internet.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-multimedia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-multimedia.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-office.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-other.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/applications-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/applications-system.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/preferences-desktop-peripherals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/preferences-desktop-peripherals.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/preferences-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/preferences-desktop.png -------------------------------------------------------------------------------- /pyside6_utils/icons/categories/preferences-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/categories/preferences-system.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/audio-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/audio-card.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/audio-input-microphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/audio-input-microphone.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/battery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/battery.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/camera-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/camera-photo.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/camera-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/camera-video.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/computer.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/drive-harddisk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/drive-harddisk.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/drive-optical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/drive-optical.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/drive-removable-media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/drive-removable-media.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/input-gaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/input-gaming.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/input-keyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/input-keyboard.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/input-mouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/input-mouse.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/media-flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/media-flash.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/media-floppy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/media-floppy.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/media-optical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/media-optical.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/multimedia-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/multimedia-player.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/network-wired.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/network-wired.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/network-wireless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/network-wireless.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/printer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/printer.png -------------------------------------------------------------------------------- /pyside6_utils/icons/devices/video-display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/devices/video-display.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emblems/emblem-favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emblems/emblem-favorite.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emblems/emblem-important.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emblems/emblem-important.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emblems/emblem-photos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emblems/emblem-photos.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emblems/emblem-readonly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emblems/emblem-readonly.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emblems/emblem-symbolic-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emblems/emblem-symbolic-link.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emblems/emblem-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emblems/emblem-system.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emblems/emblem-unreadable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emblems/emblem-unreadable.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-angel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-angel.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-crying.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-crying.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-devilish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-devilish.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-glasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-glasses.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-grin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-grin.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-kiss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-kiss.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-monkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-monkey.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-plain.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-sad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-sad.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-smile-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-smile-big.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-smile.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-surprise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-surprise.png -------------------------------------------------------------------------------- /pyside6_utils/icons/emotes/face-wink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/emotes/face-wink.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/application-certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/application-certificate.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/application-x-executable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/application-x-executable.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/audio-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/audio-x-generic.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/font-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/font-x-generic.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/image-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/image-x-generic.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/package-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/package-x-generic.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/text-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/text-html.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/text-x-generic-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/text-x-generic-template.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/text-x-generic.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/text-x-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/text-x-script.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/video-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/video-x-generic.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-address-book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-address-book.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-calendar.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-document-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-document-template.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-document.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-drawing-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-drawing-template.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-drawing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-drawing.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-presentation-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-presentation-template.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-presentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-presentation.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-spreadsheet-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-spreadsheet-template.png -------------------------------------------------------------------------------- /pyside6_utils/icons/mimetypes/x-office-spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/mimetypes/x-office-spreadsheet.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/folder-remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/folder-remote.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/folder-saved-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/folder-saved-search.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/folder.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/network-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/network-server.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/network-workgroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/network-workgroup.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/start-here.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/start-here.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/user-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/user-desktop.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/user-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/user-home.png -------------------------------------------------------------------------------- /pyside6_utils/icons/places/user-trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/places/user-trash.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/audio-volume-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/audio-volume-high.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/audio-volume-low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/audio-volume-low.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/audio-volume-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/audio-volume-medium.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/audio-volume-muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/audio-volume-muted.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/battery-caution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/battery-caution.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/dialog-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/dialog-error.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/dialog-information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/dialog-information.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/dialog-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/dialog-warning.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/folder-drag-accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/folder-drag-accept.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/folder-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/folder-open.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/folder-visiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/folder-visiting.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/image-loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/image-loading.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/image-missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/image-missing.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/mail-attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/mail-attachment.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/network-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/network-error.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/network-idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/network-idle.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/network-offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/network-offline.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/network-receive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/network-receive.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/network-transmit-receive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/network-transmit-receive.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/network-transmit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/network-transmit.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/network-wireless-encrypted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/network-wireless-encrypted.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/printer-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/printer-error.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/software-update-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/software-update-available.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/software-update-urgent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/software-update-urgent.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/user-trash-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/user-trash-full.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-clear-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-clear-night.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-clear.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-few-clouds-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-few-clouds-night.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-few-clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-few-clouds.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-overcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-overcast.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-severe-alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-severe-alert.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-showers-scattered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-showers-scattered.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-showers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-showers.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-snow.png -------------------------------------------------------------------------------- /pyside6_utils/icons/status/weather-storm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/icons/status/weather-storm.png -------------------------------------------------------------------------------- /pyside6_utils/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Init""" 2 | from .dataclass_model import DataclassModel 3 | from .dataclass_tree_item import DataclassTreeItem 4 | from .extended_sort_filter_proxy_model import ExtendedSortFilterProxyModel 5 | from .file_explorer_model import FileExplorerModel 6 | from .pandas_table_model import PandasTableModel 7 | 8 | from . import console_widget_models as console_widget_models 9 | 10 | -------------------------------------------------------------------------------- /pyside6_utils/models/console_widget_models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/models/console_widget_models/__init__.py -------------------------------------------------------------------------------- /pyside6_utils/models/console_widget_models/console_from_file_item.py: -------------------------------------------------------------------------------- 1 | """Implements the model needed to sync to a file and dynamically display the contents to a widget""" 2 | import os 3 | import time 4 | import typing 5 | 6 | from PySide6 import QtCore, QtWidgets 7 | 8 | from pyside6_utils.models.console_widget_models.console_model import \ 9 | BaseConsoleItem 10 | 11 | 12 | class FileCheckerWorker(QtCore.QObject): 13 | """A class that continuously checks a file path for changes in file size, if so, it emits a simple signal, 14 | indicating that the file has changed, used by ConsoleFromFileItem to check for changes in the file. 15 | 16 | TODO: file polling might not be the best approach, especially when using multiple files. 17 | """ 18 | fileChanged = QtCore.Signal() #Emitted when the file has changed 19 | 20 | def __init__(self, path, polling_interval : float = 0.2, *args, **kwargs) -> None: #pylint: disable=W1113 21 | super().__init__(*args, **kwargs) 22 | self._path = path 23 | self.run_flag = True #Keep track of whether the thread should keep running 24 | self._polling_interval = polling_interval #The interval in seconds to check the file for changes 25 | #TODO: make parameter 26 | self._last_size = 0 27 | 28 | def do_work(self): 29 | """Continuously check the file for changes in size, if so, emit the fileChanged signal.""" 30 | self._last_size = -1 #File doesnt exist 31 | while self.run_flag: 32 | time.sleep(self._polling_interval) 33 | 34 | if not os.path.exists(self._path): #If file does not exist, clear the text edit 35 | if self._last_size != -1: 36 | self._last_size = -1 37 | self.fileChanged.emit() 38 | continue 39 | 40 | cur_size = os.path.getsize(self._path) 41 | 42 | if cur_size != self._last_size: 43 | self._last_size = cur_size 44 | self.fileChanged.emit() 45 | 46 | 47 | class ConsoleFromFileItem(BaseConsoleItem): 48 | """An item that represents a single row in the console widget. 49 | Continually monitors the passed path for changes, and emits the current text when the file changes. 50 | E.g. we can monitor the output of a running program by calling: 51 | sys.stdout = LoggerWriter(log.info) 52 | Inside of a running process. Where LoggerWriter is an object that outputs to a file monitored by a ConsoleFromFileItem. 53 | 54 | TODO: file polling might not be the best approach, especially when using a lot of files. 55 | """ 56 | loadedLinesChanged = QtCore.Signal(list, int) #Emits all lines that have been changed, together with the line-index 57 | emitDataChanged = QtCore.Signal() #Emitted when the data of the item changes 58 | 59 | def __init__(self, name : str, path : str, *args, **kwargs): 60 | super().__init__(*args, **kwargs) 61 | self._console_pixmap = QtWidgets.QStyle.StandardPixmap.SP_TitleBarMaxButton 62 | self._console_icon = QtWidgets.QApplication.style().standardIcon(self._console_pixmap) 63 | 64 | self._name = name 65 | self._path = path 66 | 67 | # self._current_text : str = "" #The current text in the file #TODO: probably list of lines works better... 68 | self._current_line_list : list[str] = [] #List of lines 69 | self._cur_lines = [0, 0] #What lines are currently loaded 70 | self._last_edited = QtCore.QDateTime.fromSecsSinceEpoch(0) #Set to 0 so that it is always updated on first change 71 | self._current_seek : int = 0 #The current seek position in the current file 72 | 73 | 74 | #Check if file exists 75 | if self._path is None or not os.path.exists(self._path): 76 | raise ValueError(f"File {self._path} does not exist - Console Item will not be able to initiate a" 77 | "file-watcher so updates will not be shown.") 78 | 79 | 80 | #Align contents bottom 81 | self._polling_interval = 0.2 #The interval in seconds to poll the file for changes #TODO: make parameter 82 | 83 | self._current_seek : int = 0 #The current seek position in the current file 84 | self._on_content_changes_selected_file() #Call this method once to get the initial text 85 | 86 | self._file_monitor_worker = FileCheckerWorker(self._path) 87 | self._worker_thread = QtCore.QThread() 88 | self._worker_thread.started.connect(self._file_monitor_worker.do_work) 89 | self._file_monitor_worker.moveToThread(self._worker_thread) 90 | #Connect deleteLater to the finished signal of the thread 91 | self._file_monitor_worker.fileChanged.connect(self._on_content_changes_selected_file) 92 | 93 | #Connect doWork to the started signal of the thread 94 | self._worker_thread.start() 95 | 96 | 97 | def get_current_line_list(self) -> tuple[list[str], int]: 98 | """Retrieves the current text in the watched file - as currently known to the item. 99 | ICW the start-index of this buffer. When the full file is loaded, this will be 0. 100 | """ 101 | # return self._current_text, 0 #TODO: No limit implemented yet 102 | return self._current_line_list, self._cur_lines[0] 103 | 104 | 105 | def data(self, role : QtCore.Qt.ItemDataRole, column : int = 0): 106 | """Retrieve the data for the given role for this item.""" 107 | if column == 0 : 108 | return self._name 109 | elif column == 1 : 110 | return self._last_edited 111 | elif column == 2 : 112 | return self._path 113 | raise ValueError(f"Invalid role for ConsoleStandardItem: {role}") 114 | # return super().data(role) 115 | 116 | def _on_content_changes_selected_file(self, encoding="utf-8") -> None: 117 | """ 118 | When the contents of selected file changes, this method is called 119 | """ 120 | 121 | if not os.path.exists(self._path): #If file does not exist, clear the text edit 122 | self._current_line_list = [] 123 | self._current_seek = 0 124 | self.loadedLinesChanged.emit([], 0) 125 | return 126 | 127 | cur_size = os.path.getsize(self._path) 128 | 129 | #First check is size is lower than the current seek position, if so, reset the seek position (assume file reset) 130 | if cur_size < self._current_seek: 131 | # self.ui.consoleTextEdit.clear() #Also clear the text edit 132 | self._current_seek = 0 133 | self._current_text = "" #Reset the current text 134 | 135 | if cur_size <= self._current_seek: #If file size is equal to the current seek position, do nothing 136 | return 137 | 138 | cur_line = len(self._current_line_list)+1 #Get the current line number 139 | new_line_list : typing.List[str]= [] #List of new lines 140 | 141 | #Open the file and seek to the current seek position 142 | with open(self._path, "r", encoding=encoding) as in_file: 143 | in_file.seek(self._current_seek) 144 | if len(self._current_line_list) > 0 and not self._current_line_list[-1].endswith("\n"): #TODO os.linesep? 145 | self._current_line_list[-1] += in_file.readline() 146 | new_line_list.append(self._current_line_list[-1]) 147 | cur_line -= 1 #Also update the last line number 148 | for line in in_file: #Read the new lines #TODO: maybe make a bit more efficient? 149 | self._current_line_list.append(line) 150 | new_line_list.append(line) 151 | self._current_seek = in_file.tell() #Make the current seek position the end of the file 152 | 153 | #Retrieve the last edit date 154 | self._last_edited = os.path.getmtime(self._path) 155 | # self.currentTextChanged.emit(self._current_text, 0) #Emit the current text 156 | self.loadedLinesChanged.emit(new_line_list, cur_line) #Emit the current text 157 | -------------------------------------------------------------------------------- /pyside6_utils/models/console_widget_models/console_model.py: -------------------------------------------------------------------------------- 1 | """Implements the base-model for the console widget. 2 | We can then choose to implement custom sub-class of this model. 3 | """ 4 | 5 | import typing 6 | from abc import abstractmethod 7 | 8 | from PySide6 import QtCore, QtWidgets 9 | 10 | 11 | class BaseConsoleItem(QtCore.QObject): #TODO: AbstractQObjectMeta 12 | """Base-class for console items. All user-defined console items should inherit from this class. 13 | """ 14 | loadedLinesChanged = QtCore.Signal(list, int) #Emits all lines that have been changed, together with the start 15 | # line-index 16 | dataChanged = QtCore.Signal() #When the metadata of the item changes (e.g. last-edit-date, name, running-state) 17 | 18 | @abstractmethod 19 | def data(self, role : QtCore.Qt.ItemDataRole, column : int = 0): 20 | "Get the data for the passed role at the passed column" 21 | raise NotImplementedError() 22 | 23 | @abstractmethod 24 | def get_current_line_list(self) -> typing.Tuple[list[str], int]: 25 | """Get the current text (str) of this console-item 26 | 27 | Retuns: 28 | Tuple[str, int]: The current text and the start-index of this buffer 29 | """ 30 | raise NotImplementedError() 31 | 32 | 33 | class ConsoleModel(QtCore.QAbstractItemModel): 34 | """Small class to overload data-representation of the file-selection treeview based on recency 35 | and to add icons to the first column 36 | 37 | Is compatible with ConsoleFromFileItems 38 | 39 | NOTE: this model does not seem to work with treeviews, only tableviews 40 | """ 41 | def __init__(self, *args, **kwargs): 42 | super().__init__(*args, **kwargs) 43 | self._console_pixmap = QtWidgets.QStyle.StandardPixmap.SP_TitleBarMaxButton 44 | self._console_icon = QtWidgets.QApplication.style().standardIcon(self._console_pixmap) 45 | self._item_list = [] #List of ConsoleStandardItem's 46 | 47 | def columnCount(self, parent : QtCore.QModelIndex = QtCore.QModelIndex()) -> int: #pylint: disable=unused-argument 48 | return 3 49 | 50 | def removeRow(self, row: int, parent : QtCore.QModelIndex) -> bool: 51 | self.beginRemoveRows(parent, row, row) 52 | # self._item_list.pop(row) 53 | del self._item_list[row] 54 | self.endRemoveRows() 55 | self.modelReset.emit() #Why is this needed? 56 | return True 57 | 58 | def rowCount(self, parent : QtCore.QModelIndex = QtCore.QModelIndex()) -> int: 59 | if not parent.isValid(): #If model index is not valid -> top level item -> so all items 60 | return len(self._item_list) 61 | else: #If one of the sub-items 62 | return 0 63 | 64 | def parent(self, index : QtCore.QModelIndex) -> QtCore.QModelIndex: #pylint: disable=unused-argument 65 | return QtCore.QModelIndex() #No parents 66 | 67 | def index(self, row : int, column : int, parent : QtCore.QModelIndex = QtCore.QModelIndex()) -> QtCore.QModelIndex: 68 | """Return the index of the item in the model specified by the given row, column and parent index. 69 | 70 | Args: 71 | row (int): The row of the item 72 | column (int): The column of the item 73 | parent (QtCore.QModelIndex, optional): The parent index. Defaults to QtCore.QModelIndex(). 74 | 75 | Returns: 76 | QtCore.QModelIndex: The index of the item 77 | """ 78 | if not parent.isValid(): #If top-level item (should be all items actually) 79 | return self.createIndex(row, column, self._item_list[row]) 80 | else: #If item -> no children 81 | return QtCore.QModelIndex() 82 | 83 | 84 | 85 | def append_row(self, item : BaseConsoleItem): 86 | """Append a row to the model - consisting of a single ConsoleStandardItem 87 | 88 | """ 89 | self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount()) 90 | self._item_list.append(item) 91 | item.dataChanged.connect( 92 | lambda *_ : self.dataChanged.emit(self.index(self.rowCount()-1, 0), self.index(self.rowCount()-1, 2))) 93 | 94 | self.endInsertRows() 95 | 96 | def add_item (self, item : BaseConsoleItem): 97 | """Add an item to the model, same as append_row 98 | 99 | Args: 100 | item (ConsoleStandardItem): The item to add 101 | """ 102 | self.append_row(item) 103 | 104 | #Overload the data method to return bold text if changes have been made in the past x seconds 105 | def data(self, index : QtCore.QModelIndex, role : QtCore.Qt.ItemDataRole = QtCore.Qt.ItemDataRole.DisplayRole): 106 | #Check if index is valid 107 | if not index.isValid(): #if index is not valid, return None 108 | return None 109 | 110 | #Get the item from the index 111 | item = index.internalPointer() 112 | 113 | assert isinstance(item, BaseConsoleItem) 114 | 115 | if role == QtCore.Qt.ItemDataRole.DisplayRole or role == QtCore.Qt.ItemDataRole.EditRole: 116 | return item.data(role=role, column=index.column()) #Return the data (str) of the item 117 | elif role == QtCore.Qt.ItemDataRole.DecorationRole: 118 | return self._console_icon 119 | elif role == QtCore.Qt.ItemDataRole.UserRole + 1: 120 | return item 121 | else: 122 | return None 123 | # return super().data(index, role) 124 | -------------------------------------------------------------------------------- /pyside6_utils/models/dataclass_tree_item.py: -------------------------------------------------------------------------------- 1 | """Implements a single item in a dataclass-tree, representing a variable with its value""" 2 | import typing 3 | from dataclasses import Field 4 | 5 | 6 | class DataclassTreeItem(object): 7 | """ 8 | This class represents a single item in a dataclass-tree (attribute). 9 | """ 10 | def __init__(self, 11 | name : str, 12 | item_data: typing.Any, 13 | field : Field | None, 14 | parent: typing.Optional["DataclassTreeItem"] = None 15 | ) -> None: 16 | self.name = name 17 | self.item_data = item_data 18 | self.field = field 19 | self.parent_item = parent 20 | self.child_items = [] 21 | 22 | def append_child(self, item: "DataclassTreeItem") -> None: 23 | """Appends a child to this item (of same type).""" 24 | self.child_items.append(item) 25 | 26 | def child(self, row: int) -> "DataclassTreeItem": 27 | """Returns the child at the given row.""" 28 | return self.child_items[row] 29 | 30 | def child_count(self) -> int: 31 | """Returns the number of children.""" 32 | return len(self.child_items) 33 | 34 | def column_count(self) -> int: 35 | """Returns the number of columns.""" 36 | return 2 37 | 38 | def data(self) -> typing.Any: 39 | """Returns the data stored in this item.""" 40 | return self.item_data 41 | 42 | def get_field(self) -> Field | None: 43 | """Returns the field associated with this item.""" 44 | return self.field 45 | 46 | def parent(self) -> "DataclassTreeItem | None": 47 | """Returns the parent of this item.""" 48 | return self.parent_item 49 | 50 | def row(self) -> int: 51 | """Returns the row of this item.""" 52 | if self.parent_item: 53 | return self.parent_item.child_items.index(self) 54 | return 0 55 | 56 | def print(self, indent: int = 0) -> None: 57 | """Prints the tree to the console.""" 58 | print("-" * indent, self.item_data) 59 | for child in self.child_items: 60 | assert isinstance(child, DataclassTreeItem) 61 | child.print(indent + 1) 62 | -------------------------------------------------------------------------------- /pyside6_utils/models/extended_sort_filter_proxy_model.py: -------------------------------------------------------------------------------- 1 | """Implements and extended version of QSortFIlterProxyModel with extra functionality (see class docstring)""" 2 | 3 | import logging 4 | import math 5 | import numbers 6 | import typing 7 | 8 | from PySide6 import QtCore 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | 14 | FilterFunctionType =\ 15 | typing.Callable[[int, QtCore.QModelIndex | QtCore.QPersistentModelIndex, QtCore.QAbstractItemModel], bool] #Filter 16 | #function => source_row, source_parent (index), source_model 17 | 18 | class ExtendedSortFilterProxyModel(QtCore.QSortFilterProxyModel): 19 | """ 20 | Wrapper around the QSortFilterProxyModel which enables the use of multiple sort-columns instead of just one. 21 | If 2 items are not-sortable by column 1, we sort by column 2, then 3 etc. etc. 22 | 23 | Also implements a function-list based custom filtering system. 24 | Using setFilterFunction, the user can add a function that is used to AND-filter the rows. 25 | Filter-functions take a row as argument and return a bool. If the function returns True, the row is accepted. 26 | 27 | NOTE: Filtering is AND-ed with the default filterAcceptsRow function, so we can still use the default functionality 28 | in addition to the custom functions. 29 | """ 30 | 31 | def __init__(self, parent: QtCore.QObject | None = ...) -> None: 32 | super().__init__(parent) 33 | self._sort_columns : list[int] = [] 34 | self._sort_orders : list[QtCore.Qt.SortOrder] = [] 35 | self._filter_functions : typing.Dict[str, FilterFunctionType] = {} 36 | 37 | 38 | def _val_less_than(self, leftval, rightval): 39 | if leftval is None or (isinstance(leftval, numbers.Number) and math.isnan(leftval)): #type: ignore 40 | return True 41 | elif rightval is None or (isinstance(rightval, numbers.Number) and math.isnan(rightval)): #type: ignore 42 | return False 43 | return leftval < rightval #type: ignore 44 | 45 | def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: 46 | """ 47 | Reimplemented from QSortFilterProxyModel. 48 | """ 49 | 50 | if len(self._sort_columns) == 0: #If using default behaviour, use the default implementation 51 | return super().lessThan(left, right) 52 | else: 53 | for column, order in zip(self._sort_columns, self._sort_orders): 54 | left = self.sourceModel().index(left.row(), column) 55 | right = self.sourceModel().index(right.row(), column) 56 | leftval = left.data(role=QtCore.Qt.ItemDataRole.EditRole) 57 | rightval = right.data(role=QtCore.Qt.ItemDataRole.EditRole) 58 | if self._val_less_than(leftval, rightval) == self._val_less_than(leftval=rightval, rightval=leftval): 59 | #If the values are equal, continue to the next column 60 | continue 61 | else: 62 | return self._val_less_than(leftval, rightval) if \ 63 | order == QtCore.Qt.SortOrder.AscendingOrder else self._val_less_than(leftval, rightval) 64 | 65 | return False #If we can't differentiate the rows, return False (i.e. don't swap them) 66 | 67 | 68 | def sort_by_columns(self, columns: list[int], orders: list[QtCore.Qt.SortOrder] | None = None) -> None: 69 | """ 70 | Sets the sort-columns and their respective sort-orders. 71 | 72 | :param columns: The columns to sort by. 73 | :param orders: The sort-orders to use for the columns. If None, the default sort-order is used. 74 | """ 75 | if orders is None or orders == []: #If no orders are specified, use the default order 76 | orders = [QtCore.Qt.SortOrder.AscendingOrder] * len(columns) 77 | self._sort_columns = columns 78 | self._sort_orders = orders 79 | self.invalidate() 80 | 81 | def set_filter_function(self, 82 | function_name : str, 83 | function : FilterFunctionType 84 | ): 85 | """ 86 | Adds a filter function to the proxy model. All filter-functions together are AND-ed with the default 87 | filterAcceptsRow function to determine if a row is accepted. 88 | 89 | NOTE: overwrites any existing filter function with the same name, if it does, filters are invalidated 90 | 91 | Args: 92 | function_name (str): The name of the filter function (used to identify it) 93 | function (FILTER_FUNCTION_TYPE): The filter function to add 94 | """ 95 | invalidate = False 96 | if function_name in self._filter_functions: 97 | invalidate = True 98 | self._filter_functions[function_name] = function 99 | if invalidate: 100 | self.invalidateFilter() 101 | 102 | 103 | def clear_function_filters(self): 104 | """Clear all filter functions""" 105 | self._filter_functions = {} 106 | self.invalidateFilter() 107 | 108 | def get_filter_functions(self) -> typing.Dict[str, FilterFunctionType]: 109 | """Returns a dict of all filter functions""" 110 | return self._filter_functions 111 | 112 | def filterAcceptsRow(self, 113 | source_row: int, 114 | source_parent: QtCore.QModelIndex | QtCore.QPersistentModelIndex 115 | ) -> bool: 116 | """Calls QSortFilterProxyModel.filterAcceptsRow and ANDs the result with all filter functions 117 | 118 | NOTE: if any filter function raises an exception on a row, the row is not accepted and the error is logged. 119 | All filter functions are "AND-ed" together with the default filterAcceptsRow function. 120 | """ 121 | if not super().filterAcceptsRow(source_row, source_parent): 122 | return False 123 | for function in self._filter_functions.values(): 124 | try: 125 | if not function(source_row, source_parent, self.sourceModel()): 126 | return False 127 | except Exception as exception: #pylint: disable=broad-except 128 | log.error(f"Error while filtering row {exception} - {type(exception).__name__}: {exception}") 129 | return False 130 | return True 131 | -------------------------------------------------------------------------------- /pyside6_utils/models/file_explorer_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements QFileSystemModel with the added ability to highlight a single file - which puts an icon next to the file 3 | and sets it to bold. 4 | """ 5 | import logging 6 | import os 7 | import typing 8 | 9 | from PySide6 import QtCore, QtGui, QtWidgets 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | class FileExplorerModel(QtWidgets.QFileSystemModel): 14 | """A QFileSystemModel that also allows for highlighting of a single file, for example when selecting a file for 15 | editing purposes. 16 | """ 17 | highlightPathChanged = QtCore.Signal(str) 18 | 19 | def __init__(self, parent: typing.Optional[QtCore.QObject] = None, allow_select_files_only=True) -> None: 20 | super().__init__(parent) 21 | self._selected_path = None 22 | 23 | self._selection_pixmap = QtWidgets.QStyle.StandardPixmap.SP_DialogApplyButton #Checkmark 24 | self._selection_icon = QtWidgets.QApplication.style().standardIcon(self._selection_pixmap) 25 | self._prev_selection = None 26 | 27 | if allow_select_files_only: 28 | self._allow_select_files_only = True 29 | 30 | def reset_highlight(self) -> None: 31 | """Reset the highlight""" 32 | self._selected_path = None 33 | if self._prev_selection: 34 | self.dataChanged.emit(self._prev_selection, self._prev_selection) #Update the icon of prev. highlighted item 35 | self._prev_selection = None 36 | self.highlightPathChanged.emit(None) 37 | 38 | def get_highlight_path(self) -> str | None: 39 | """Get the currently highlighted path""" 40 | return self._selected_path 41 | 42 | def set_highlight_using_path(self, path: str | None) -> None: 43 | """Set the current highlight to the given path 44 | 45 | Args: 46 | path (str | None): The path to highlight (or None to reset) 47 | """ 48 | self._selected_path = path 49 | self.highlightPathChanged.emit(self._selected_path) 50 | 51 | def set_highlight_using_index(self, selection: QtCore.QModelIndex | QtCore.QPersistentModelIndex) -> None: 52 | """Set the current selection to the model""" 53 | log.info(f"Trying to set model selection to: {self.filePath(selection)}") 54 | 55 | if self._allow_select_files_only and not os.path.isfile(self.filePath(selection)): #Skip selection if only 56 | #files are allowed and the selection is not a file 57 | log.warning(f"Selection is not a file, skipping selection: {self.filePath(selection)}") 58 | return 59 | 60 | self._selected_path = self.filePath(selection) 61 | 62 | if self._prev_selection: 63 | log.debug(f"Updating previous selection: {self.filePath(self._prev_selection)}") 64 | self.dataChanged.emit( 65 | self._prev_selection, 66 | self.index( 67 | self._prev_selection.row(), 68 | self.columnCount(self._prev_selection), 69 | self._prev_selection.parent() 70 | )) #Update the icon of prev. highlighted item 71 | 72 | new_selection = self.index(selection.row(), 0, selection.parent()) #Get index of first column (icon) 73 | self._prev_selection = QtCore.QPersistentModelIndex(new_selection) 74 | self.dataChanged.emit( 75 | new_selection, 76 | self.index( 77 | new_selection.row(), 78 | self.columnCount(new_selection), 79 | new_selection.parent() 80 | ) 81 | ) 82 | self.highlightPathChanged.emit(self._selected_path) 83 | 84 | 85 | 86 | def data(self, 87 | index: QtCore.QModelIndex, 88 | role: int = QtCore.Qt.ItemDataRole.DisplayRole) -> typing.Any: 89 | 90 | #TODO: what if highlighted file changed? (e.g. file renamed/removed) 91 | 92 | #If first column and fileIconRole, return selection icon 93 | if role == QtWidgets.QFileSystemModel.Roles.FileIconRole \ 94 | and index.column() == 0 \ 95 | and self._selected_path \ 96 | and (self.filePath(index) == self._selected_path): 97 | #Return arrow icon if selected 98 | return self._selection_icon 99 | elif role == QtCore.Qt.ItemDataRole.FontRole \ 100 | and self._selected_path \ 101 | and (self.filePath(index) == self._selected_path): #Enbolden whole selected column 102 | font = QtGui.QFont() 103 | font.setBold(True) 104 | return font 105 | 106 | 107 | return super().data(index, role) 108 | 109 | 110 | 111 | if __name__ == "__main__": 112 | print("Testing FileExplorerModel") 113 | app = QtWidgets.QApplication([]) 114 | # model = FileExplorerModel() 115 | model = QtWidgets.QFileSystemModel() 116 | 117 | #Get the current path 118 | current_path = os.path.dirname(os.path.realpath(__file__)) 119 | 120 | #Set editable 121 | model.setReadOnly(False) 122 | print(current_path) 123 | model.setRootPath(current_path) 124 | 125 | 126 | view = QtWidgets.QTreeView() 127 | view.setModel(model) 128 | view.setRootIndex(model.index(current_path)) 129 | view.show() 130 | 131 | app.exec() 132 | 133 | 134 | print("Done testing FileExplorerModel") 135 | -------------------------------------------------------------------------------- /pyside6_utils/models/pandas_table_model.py: -------------------------------------------------------------------------------- 1 | """Implements the a Qt-Model for pandas dataframes, so we can display them as a table in Qt-Widgets""" 2 | import logging 3 | from numbers import Number 4 | 5 | import pandas as pd 6 | from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class PandasTableModel(QAbstractTableModel): 12 | """ A model-wrapper around a pandas dataframe to use with a QTableView 13 | NOTE: editing is not supported (yet) 14 | """ 15 | 16 | def __init__(self, dataframe: pd.DataFrame, parent=None): 17 | QAbstractTableModel.__init__(self, parent) 18 | self._dataframe = dataframe 19 | 20 | def rowCount(self, parent=QModelIndex()) -> int: 21 | if parent == QModelIndex(): 22 | return len(self._dataframe) 23 | return 0 24 | 25 | def columnCount(self, parent=QModelIndex()) -> int: 26 | if parent == QModelIndex(): 27 | return len(self._dataframe.columns) 28 | return 0 29 | 30 | def data(self, index: QModelIndex, role=Qt.ItemDataRole): 31 | if not index.isValid(): 32 | return None 33 | 34 | if role == Qt.ItemDataRole.DisplayRole: 35 | data = self._dataframe.iloc[index.row(), index.column()] 36 | if data is None or (isinstance(data, Number) and pd.isnull(data)): 37 | return "" 38 | #TODO: Convert item to qt-equivalent instead of string? 39 | return str(data) 40 | elif role == Qt.ItemDataRole.EditRole: 41 | return self._dataframe.iloc[index.row(), index.column()] 42 | elif role == Qt.ItemDataRole.BackgroundRole: 43 | return None 44 | 45 | return None 46 | 47 | 48 | def headerData( 49 | self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole 50 | ): 51 | if role == Qt.ItemDataRole.DisplayRole: 52 | if orientation == Qt.Orientation.Horizontal: 53 | return str(self._dataframe.columns[section]) 54 | 55 | if orientation == Qt.Orientation.Vertical: 56 | return str(self._dataframe.index[section]) 57 | return None 58 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/registrars/__init__.py -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_collapsible_group_box.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | 6 | import os 7 | 8 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 9 | from pyside6_utils.utility.utility_functions import snakecase 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.collapsible_group_box import CollapsibleGroupBox 13 | 14 | BASE_NAME = CollapsibleGroupBox.__name__[0].lower() + CollapsibleGroupBox.__name__[1:] 15 | print(BASE_NAME) 16 | DOM_XML = f""" 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 400 24 | 200 25 | 26 | 27 | 28 | 29 | """ 30 | MODULE = "" 31 | 32 | if len(Paths.PACKAGE_NAME) > 0: 33 | MODULE+= f"{Paths.PACKAGE_NAME}." 34 | if len(Paths.WIDGETS_SUBPATH) > 0: 35 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 36 | MODULE += snakecase(CollapsibleGroupBox.__name__) #Uses snakecase 37 | 38 | 39 | QPyDesignerCustomWidgetCollection.registerCustomWidget(CollapsibleGroupBox, 40 | module=MODULE, 41 | tool_tip=CollapsibleGroupBox.DESCRIPTION, 42 | xml=DOM_XML, 43 | container=True, 44 | group="Containers") 45 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_console_widget.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.console_widget import ConsoleWidget 13 | 14 | BASE_NAME = ConsoleWidget.__name__[0].lower() + ConsoleWidget.__name__[1:] 15 | 16 | DOM_XML = f""" 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 400 24 | 200 25 | 26 | 27 | 28 | 29 | """ 30 | 31 | MODULE = "" 32 | if len(Paths.PACKAGE_NAME) > 0: 33 | MODULE+= f"{Paths.PACKAGE_NAME}." 34 | if len(Paths.WIDGETS_SUBPATH) > 0: 35 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." #NOTE: assumes the file-name is the same as the class-name 36 | MODULE += snakecase(ConsoleWidget.__name__) #Uses snakecase 37 | 38 | QPyDesignerCustomWidgetCollection.registerCustomWidget(ConsoleWidget, 39 | module=MODULE, 40 | tool_tip=ConsoleWidget.DESCRIPTION, 41 | xml=DOM_XML, 42 | container=False, 43 | group="Item Views (Model-Based)") 44 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_extended_mdi_area.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.extended_mdi_area import ExtendedMdiArea 13 | 14 | BASE_NAME = ExtendedMdiArea.__name__[0].lower() + ExtendedMdiArea.__name__[1:] 15 | 16 | DOM_XML = f""" 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 400 24 | 200 25 | 26 | 27 | 28 | 29 | """ 30 | 31 | MODULE = "" 32 | if len(Paths.PACKAGE_NAME) > 0: 33 | MODULE+= f"{Paths.PACKAGE_NAME}." 34 | if len(Paths.WIDGETS_SUBPATH) > 0: 35 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 36 | MODULE += snakecase(ExtendedMdiArea.__name__) #Uses snakecase 37 | 38 | QPyDesignerCustomWidgetCollection.registerCustomWidget(ExtendedMdiArea, 39 | module=MODULE, 40 | tool_tip=ExtendedMdiArea.DESCRIPTION, 41 | xml=DOM_XML, 42 | container=True, 43 | group="Containers") 44 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_file_explorer_view.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.file_explorer_view import FileExplorerView 13 | 14 | 15 | BASE_NAME = FileExplorerView.__name__[0].lower() + FileExplorerView.__name__[1:] 16 | 17 | DOM_XML = f""" 18 | 19 | 20 | 21 | 22 | 0 23 | 0 24 | 400 25 | 200 26 | 27 | 28 | 29 | 30 | 31 | """ 32 | 33 | MODULE = "" 34 | if len(Paths.PACKAGE_NAME) > 0: 35 | MODULE+= f"{Paths.PACKAGE_NAME}." 36 | if len(Paths.WIDGETS_SUBPATH) > 0: 37 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 38 | MODULE += snakecase(FileExplorerView.__name__) #Uses snakecase 39 | 40 | QPyDesignerCustomWidgetCollection.registerCustomWidget(FileExplorerView, 41 | module=MODULE,#f"Widgets.{FileExplorerView.__name__}", 42 | tool_tip=FileExplorerView.DESCRIPTION, 43 | xml=DOM_XML, 44 | container=False, 45 | group="Item Views (Model-Based)") 46 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_overlay_widget.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.overlay_widget import OverlayWidget 13 | 14 | BASE_NAME = OverlayWidget.__name__[0].lower() + OverlayWidget.__name__[1:] 15 | 16 | DOM_XML = f""" 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 400 24 | 200 25 | 26 | 27 | 28 | True 29 | 30 | 31 | 32 | 33 | """ 34 | 35 | MODULE = "" 36 | if len(Paths.PACKAGE_NAME) > 0: 37 | MODULE+= f"{Paths.PACKAGE_NAME}." 38 | if len(Paths.WIDGETS_SUBPATH) > 0: 39 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 40 | MODULE += snakecase(OverlayWidget.__name__) #Uses snakecase 41 | 42 | QPyDesignerCustomWidgetCollection.registerCustomWidget(OverlayWidget, 43 | module=MODULE,#f"Widgets.{OverlayWidget.__name__}", 44 | tool_tip=OverlayWidget.DESCRIPTION, 45 | xml=DOM_XML, 46 | container=True, 47 | group="Containers") 48 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_pandas_table.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.pandas_table_view import PandasTableView 13 | 14 | 15 | BASE_NAME = PandasTableView.__name__[0].lower() + PandasTableView.__name__[1:] 16 | 17 | DOM_XML = f""" 18 | 19 | 20 | 21 | 22 | 0 23 | 0 24 | 400 25 | 200 26 | 27 | 28 | 29 | 30 | 31 | """ 32 | 33 | MODULE = "" 34 | if len(Paths.PACKAGE_NAME) > 0: 35 | MODULE+= f"{Paths.PACKAGE_NAME}." 36 | if len(Paths.WIDGETS_SUBPATH) > 0: 37 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 38 | MODULE += snakecase(PandasTableView.__name__) #Uses snakecase 39 | 40 | QPyDesignerCustomWidgetCollection.registerCustomWidget(PandasTableView, 41 | module=MODULE, 42 | tool_tip=PandasTableView.DESCRIPTION, 43 | xml=DOM_XML, 44 | container=True, 45 | group="Item Views (Model-Based)") 46 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_range_selector.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.range_selector import RangeSelector 13 | 14 | BASE_NAME = RangeSelector.__name__[0].lower() + RangeSelector.__name__[1:] 15 | 16 | DOM_XML = f""" 17 | 18 | 19 | 20 | 21 | """ 22 | 23 | MODULE = "" 24 | if len(Paths.PACKAGE_NAME) > 0: 25 | MODULE+= f"{Paths.PACKAGE_NAME}." 26 | if len(Paths.WIDGETS_SUBPATH) > 0: 27 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 28 | MODULE += snakecase(RangeSelector.__name__) #Uses snakecase 29 | 30 | QPyDesignerCustomWidgetCollection.registerCustomWidget(RangeSelector, 31 | module=MODULE, 32 | tool_tip=RangeSelector.DESCRIPTION, 33 | xml=DOM_XML, 34 | container=False, 35 | group="Input Widgets") 36 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_square_frame.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.square_frame import SquareFrame 13 | 14 | 15 | BASE_NAME = SquareFrame.__name__[0].lower() + SquareFrame.__name__[1:] 16 | 17 | DOM_XML = f""" 18 | 19 | 20 | 21 | 22 | """ 23 | 24 | MODULE = "" 25 | if len(Paths.PACKAGE_NAME) > 0: 26 | MODULE+= f"{Paths.PACKAGE_NAME}." 27 | if len(Paths.WIDGETS_SUBPATH) > 0: 28 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 29 | MODULE += snakecase(SquareFrame.__name__) #Uses snakecase 30 | 31 | QPyDesignerCustomWidgetCollection.registerCustomWidget(SquareFrame, 32 | module=MODULE, 33 | tool_tip=SquareFrame.DESCRIPTION, 34 | xml=DOM_XML, 35 | container=True, 36 | group="Containers") 37 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_widget_list.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.widget_list import WidgetList 13 | 14 | 15 | BASE_NAME = WidgetList.__name__[0].lower() + WidgetList.__name__[1:] 16 | 17 | DOM_XML = f""" 18 | 19 | 20 | 21 | 22 | 0 23 | 0 24 | 200 25 | 300 26 | 27 | 28 | 29 | 30 | """ 31 | MODULE = "" 32 | if len(Paths.PACKAGE_NAME) > 0: 33 | MODULE+= f"{Paths.PACKAGE_NAME}." 34 | if len(Paths.WIDGETS_SUBPATH) > 0: 35 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 36 | MODULE += snakecase(WidgetList.__name__) #Uses snakecase 37 | 38 | QPyDesignerCustomWidgetCollection.registerCustomWidget(WidgetList, 39 | module=MODULE, 40 | tool_tip=WidgetList.DESCRIPTION, 41 | xml=DOM_XML, 42 | container=False, 43 | group="Item Widgets (Item-Based)") 44 | -------------------------------------------------------------------------------- /pyside6_utils/registrars/register_widget_switcher.py: -------------------------------------------------------------------------------- 1 | """For use by QtDesigner. If this folder is passed to Qt-Designer by using the environment variable 2 | PYSIDE_DESIGNER_PLUGINS= when launching designer, the registered widget will appear and 3 | will be usable in Qt-Designer. 4 | """ 5 | import os 6 | 7 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 8 | from pyside6_utils.utility.utility_functions import snakecase 9 | 10 | 11 | from pyside6_utils.constants import Paths 12 | from pyside6_utils.widgets.widget_switcher import WidgetSwitcher 13 | 14 | 15 | BASE_NAME = WidgetSwitcher.__name__[0].lower() + WidgetSwitcher.__name__[1:] 16 | 17 | DOM_XML = f""" 18 | 19 | 20 | 21 | 22 | 0 23 | 0 24 | 100 25 | 24 26 | 27 | 28 | 29 | 30 | """ 31 | MODULE = "" 32 | if len(Paths.PACKAGE_NAME) > 0: 33 | MODULE+= f"{Paths.PACKAGE_NAME}." 34 | if len(Paths.WIDGETS_SUBPATH) > 0: 35 | MODULE+= f"{Paths.WIDGETS_SUBPATH.replace(os.sep, '.')}." 36 | MODULE += snakecase(WidgetSwitcher.__name__) #Uses snakecase 37 | 38 | QPyDesignerCustomWidgetCollection.registerCustomWidget(WidgetSwitcher, 39 | module=MODULE, 40 | tool_tip=WidgetSwitcher.DESCRIPTION, 41 | xml=DOM_XML, 42 | container=True, 43 | group="Containers") 44 | -------------------------------------------------------------------------------- /pyside6_utils/ui/AllWidgets.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1287 10 | 959 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | CollapsibleGroupBox 22 | 23 | 24 | false 25 | 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore 34 | 35 | 36 | 37 | 38 | 39 | 40 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | true 52 | 53 | 54 | 55 | Pandas Model/TableView: 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Qt::Horizontal 66 | 67 | 68 | false 69 | 70 | 71 | false 72 | 73 | 74 | QSlider::TicksAbove 75 | 76 | 77 | false 78 | 79 | 80 | true 81 | 82 | 83 | 84 | 85 | 86 | 87 | Qt::Vertical 88 | 89 | 90 | QSizePolicy::Expanding 91 | 92 | 93 | 94 | 20 95 | 456 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 0 106 | 0 107 | 1287 108 | 22 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | CollapsibleGroupBox 117 | QGroupBox 118 |
pyside6_utils.widgets.collapsible_group_box
119 | 1 120 |
121 | 122 | PandasTableView 123 | QTableView 124 |
pyside6_utils.widgets.pandas_table_view
125 | 1 126 |
127 | 128 | RangeSelector 129 | QSlider 130 |
pyside6_utils.widgets.range_selector
131 |
132 |
133 | 134 | 135 | 136 | 137 |
138 | -------------------------------------------------------------------------------- /pyside6_utils/ui/ConsoleWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ConsoleWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1466 10 | 346 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | Qt::Horizontal 36 | 37 | 38 | 5 39 | 40 | 41 | 42 | 43 | 0 44 | 0 45 | 46 | 47 | 48 | Qt::ScrollBarAlwaysOn 49 | 50 | 51 | QPlainTextEdit::NoWrap 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /pyside6_utils/ui/ConsoleWidget_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'ConsoleWidget.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.5.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QHeaderView, QPlainTextEdit, QSizePolicy, 19 | QSplitter, QTableView, QVBoxLayout, QWidget) 20 | import pyside6_utils.icons.app_resources_rc 21 | 22 | class Ui_ConsoleWidget(object): 23 | def setupUi(self, ConsoleWidget): 24 | if not ConsoleWidget.objectName(): 25 | ConsoleWidget.setObjectName(u"ConsoleWidget") 26 | ConsoleWidget.resize(1466, 346) 27 | self.verticalLayout = QVBoxLayout(ConsoleWidget) 28 | self.verticalLayout.setSpacing(0) 29 | self.verticalLayout.setObjectName(u"verticalLayout") 30 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 31 | self.splitter = QSplitter(ConsoleWidget) 32 | self.splitter.setObjectName(u"splitter") 33 | self.splitter.setOrientation(Qt.Horizontal) 34 | self.splitter.setHandleWidth(5) 35 | self.consoleTextEdit = QPlainTextEdit(self.splitter) 36 | self.consoleTextEdit.setObjectName(u"consoleTextEdit") 37 | sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 38 | sizePolicy.setHorizontalStretch(0) 39 | sizePolicy.setVerticalStretch(0) 40 | sizePolicy.setHeightForWidth(self.consoleTextEdit.sizePolicy().hasHeightForWidth()) 41 | self.consoleTextEdit.setSizePolicy(sizePolicy) 42 | self.consoleTextEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 43 | self.consoleTextEdit.setLineWrapMode(QPlainTextEdit.NoWrap) 44 | self.splitter.addWidget(self.consoleTextEdit) 45 | self.fileSelectionTableView = QTableView(self.splitter) 46 | self.fileSelectionTableView.setObjectName(u"fileSelectionTableView") 47 | self.splitter.addWidget(self.fileSelectionTableView) 48 | 49 | self.verticalLayout.addWidget(self.splitter) 50 | 51 | 52 | self.retranslateUi(ConsoleWidget) 53 | 54 | QMetaObject.connectSlotsByName(ConsoleWidget) 55 | # setupUi 56 | 57 | def retranslateUi(self, ConsoleWidget): 58 | ConsoleWidget.setWindowTitle(QCoreApplication.translate("ConsoleWidget", u"Form", None)) 59 | self.consoleTextEdit.setPlainText("") 60 | # retranslateUi 61 | 62 | -------------------------------------------------------------------------------- /pyside6_utils/ui/FramelessMdiWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FramelessMidiWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 424 10 | 463 11 | 12 | 13 | 14 | 15 | 60 16 | 60 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | false 24 | 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 0 40 | 41 | 42 | 43 | 44 | 0 45 | 46 | 47 | 48 | 49 | true 50 | 51 | 52 | QFrame::StyledPanel 53 | 54 | 55 | QFrame::Plain 56 | 57 | 58 | 2 59 | 60 | 61 | 62 | 0 63 | 64 | 65 | 0 66 | 67 | 68 | 0 69 | 70 | 71 | 0 72 | 73 | 74 | 0 75 | 76 | 77 | 78 | 79 | 80 | 0 81 | 0 82 | 83 | 84 | 85 | QFrame::StyledPanel 86 | 87 | 88 | QFrame::Plain 89 | 90 | 91 | 3 92 | 93 | 94 | 95 | 0 96 | 97 | 98 | 0 99 | 100 | 101 | 0 102 | 103 | 104 | 0 105 | 106 | 107 | 0 108 | 109 | 110 | 111 | 112 | 113 | true 114 | 115 | 116 | 117 | WindowTitle 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 0 126 | 0 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | :/icons/actions/list-remove.png:/icons/actions/list-remove.png 135 | 136 | 137 | 138 | 15 139 | 15 140 | 141 | 142 | 143 | false 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 0 152 | 0 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | :/icons/actions/system-search.png:/icons/actions/system-search.png 161 | 162 | 163 | 164 | 15 165 | 15 166 | 167 | 168 | 169 | false 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /pyside6_utils/ui/FramelessMdiWindow_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'FramelessMdiWindow.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.5.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QLabel, 19 | QPushButton, QSizePolicy, QVBoxLayout, QWidget) 20 | import pyside6_utils.icons.app_resources_rc 21 | 22 | class Ui_FramelessMidiWindow(object): 23 | def setupUi(self, FramelessMidiWindow): 24 | if not FramelessMidiWindow.objectName(): 25 | FramelessMidiWindow.setObjectName(u"FramelessMidiWindow") 26 | FramelessMidiWindow.resize(424, 463) 27 | FramelessMidiWindow.setMinimumSize(QSize(60, 60)) 28 | FramelessMidiWindow.setAutoFillBackground(False) 29 | self.verticalLayout_4 = QVBoxLayout(FramelessMidiWindow) 30 | self.verticalLayout_4.setSpacing(0) 31 | self.verticalLayout_4.setObjectName(u"verticalLayout_4") 32 | self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) 33 | self.verticalLayout_3 = QVBoxLayout() 34 | self.verticalLayout_3.setSpacing(0) 35 | self.verticalLayout_3.setObjectName(u"verticalLayout_3") 36 | self.frame = QFrame(FramelessMidiWindow) 37 | self.frame.setObjectName(u"frame") 38 | self.frame.setAutoFillBackground(True) 39 | self.frame.setFrameShape(QFrame.StyledPanel) 40 | self.frame.setFrameShadow(QFrame.Plain) 41 | self.frame.setLineWidth(2) 42 | self.verticalLayout_2 = QVBoxLayout(self.frame) 43 | self.verticalLayout_2.setSpacing(0) 44 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 45 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 46 | self.titleBar = QFrame(self.frame) 47 | self.titleBar.setObjectName(u"titleBar") 48 | sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 49 | sizePolicy.setHorizontalStretch(0) 50 | sizePolicy.setVerticalStretch(0) 51 | sizePolicy.setHeightForWidth(self.titleBar.sizePolicy().hasHeightForWidth()) 52 | self.titleBar.setSizePolicy(sizePolicy) 53 | self.titleBar.setFrameShape(QFrame.StyledPanel) 54 | self.titleBar.setFrameShadow(QFrame.Plain) 55 | self.titleBar.setLineWidth(3) 56 | self.horizontalLayout = QHBoxLayout(self.titleBar) 57 | self.horizontalLayout.setSpacing(0) 58 | self.horizontalLayout.setObjectName(u"horizontalLayout") 59 | self.horizontalLayout.setContentsMargins(0, 0, 0, 0) 60 | self.titleLabel = QLabel(self.titleBar) 61 | self.titleLabel.setObjectName(u"titleLabel") 62 | font = QFont() 63 | font.setBold(True) 64 | self.titleLabel.setFont(font) 65 | 66 | self.horizontalLayout.addWidget(self.titleLabel) 67 | 68 | self.MinimizeButton = QPushButton(self.titleBar) 69 | self.MinimizeButton.setObjectName(u"MinimizeButton") 70 | sizePolicy1 = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) 71 | sizePolicy1.setHorizontalStretch(0) 72 | sizePolicy1.setVerticalStretch(0) 73 | sizePolicy1.setHeightForWidth(self.MinimizeButton.sizePolicy().hasHeightForWidth()) 74 | self.MinimizeButton.setSizePolicy(sizePolicy1) 75 | icon = QIcon() 76 | icon.addFile(u":/icons/actions/list-remove.png", QSize(), QIcon.Normal, QIcon.Off) 77 | self.MinimizeButton.setIcon(icon) 78 | self.MinimizeButton.setIconSize(QSize(15, 15)) 79 | self.MinimizeButton.setFlat(False) 80 | 81 | self.horizontalLayout.addWidget(self.MinimizeButton) 82 | 83 | self.zoomButton = QPushButton(self.titleBar) 84 | self.zoomButton.setObjectName(u"zoomButton") 85 | sizePolicy1.setHeightForWidth(self.zoomButton.sizePolicy().hasHeightForWidth()) 86 | self.zoomButton.setSizePolicy(sizePolicy1) 87 | icon1 = QIcon() 88 | icon1.addFile(u":/icons/actions/system-search.png", QSize(), QIcon.Normal, QIcon.Off) 89 | self.zoomButton.setIcon(icon1) 90 | self.zoomButton.setIconSize(QSize(15, 15)) 91 | self.zoomButton.setFlat(False) 92 | 93 | self.horizontalLayout.addWidget(self.zoomButton) 94 | 95 | 96 | self.verticalLayout_2.addWidget(self.titleBar) 97 | 98 | self.contentLayout = QVBoxLayout() 99 | self.contentLayout.setObjectName(u"contentLayout") 100 | 101 | self.verticalLayout_2.addLayout(self.contentLayout) 102 | 103 | self.verticalLayout_2.setStretch(1, 1) 104 | 105 | self.verticalLayout_3.addWidget(self.frame) 106 | 107 | 108 | self.verticalLayout_4.addLayout(self.verticalLayout_3) 109 | 110 | 111 | self.retranslateUi(FramelessMidiWindow) 112 | 113 | QMetaObject.connectSlotsByName(FramelessMidiWindow) 114 | # setupUi 115 | 116 | def retranslateUi(self, FramelessMidiWindow): 117 | FramelessMidiWindow.setWindowTitle(QCoreApplication.translate("FramelessMidiWindow", u"Form", None)) 118 | self.titleLabel.setText(QCoreApplication.translate("FramelessMidiWindow", u"WindowTitle", None)) 119 | self.MinimizeButton.setText("") 120 | self.zoomButton.setText("") 121 | # retranslateUi 122 | 123 | -------------------------------------------------------------------------------- /pyside6_utils/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Woutah/pyside6-utils/0230a1380966edd95d3cd746233b35c56e7bf7ee/pyside6_utils/ui/__init__.py -------------------------------------------------------------------------------- /pyside6_utils/ui/allWidgets_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'AllWidgets.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.5.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QHeaderView, QLabel, QMainWindow, 19 | QMenuBar, QSizePolicy, QSlider, QSpacerItem, 20 | QStatusBar, QVBoxLayout, QWidget) 21 | 22 | from pyside6_utils.widgets.collapsible_group_box import CollapsibleGroupBox 23 | from pyside6_utils.widgets.pandas_table_view import PandasTableView 24 | from pyside6_utils.widgets.range_selector import RangeSelector 25 | import pyside6_utils.icons.app_resources_rc 26 | 27 | class Ui_MainWindow(object): 28 | def setupUi(self, MainWindow): 29 | if not MainWindow.objectName(): 30 | MainWindow.setObjectName(u"MainWindow") 31 | MainWindow.resize(1287, 959) 32 | self.centralwidget = QWidget(MainWindow) 33 | self.centralwidget.setObjectName(u"centralwidget") 34 | self.verticalLayout = QVBoxLayout(self.centralwidget) 35 | self.verticalLayout.setObjectName(u"verticalLayout") 36 | self.collapsibleGroupBox = CollapsibleGroupBox(self.centralwidget) 37 | self.collapsibleGroupBox.setObjectName(u"collapsibleGroupBox") 38 | self.collapsibleGroupBox.setProperty("collapsesHorizontal", False) 39 | self.collapsibleGroupBox.setProperty("makeFlatWhenCollapsed", True) 40 | self.verticalLayout_2 = QVBoxLayout(self.collapsibleGroupBox) 41 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 42 | self.label = QLabel(self.collapsibleGroupBox) 43 | self.label.setObjectName(u"label") 44 | 45 | self.verticalLayout_2.addWidget(self.label) 46 | 47 | self.label_2 = QLabel(self.collapsibleGroupBox) 48 | self.label_2.setObjectName(u"label_2") 49 | 50 | self.verticalLayout_2.addWidget(self.label_2) 51 | 52 | 53 | self.verticalLayout.addWidget(self.collapsibleGroupBox) 54 | 55 | self.label_3 = QLabel(self.centralwidget) 56 | self.label_3.setObjectName(u"label_3") 57 | font = QFont() 58 | font.setBold(True) 59 | self.label_3.setFont(font) 60 | 61 | self.verticalLayout.addWidget(self.label_3) 62 | 63 | self.pandasTableView = PandasTableView(self.centralwidget) 64 | self.pandasTableView.setObjectName(u"pandasTableView") 65 | 66 | self.verticalLayout.addWidget(self.pandasTableView) 67 | 68 | self.rangeSelector = RangeSelector(self.centralwidget) 69 | self.rangeSelector.setObjectName(u"rangeSelector") 70 | self.rangeSelector.setOrientation(Qt.Horizontal) 71 | self.rangeSelector.setInvertedAppearance(False) 72 | self.rangeSelector.setInvertedControls(False) 73 | self.rangeSelector.setTickPosition(QSlider.TicksAbove) 74 | self.rangeSelector.setProperty("spanOnGroove", False) 75 | self.rangeSelector.setProperty("hasValueBoxes", True) 76 | 77 | self.verticalLayout.addWidget(self.rangeSelector) 78 | 79 | self.verticalSpacer = QSpacerItem(20, 456, QSizePolicy.Minimum, QSizePolicy.Expanding) 80 | 81 | self.verticalLayout.addItem(self.verticalSpacer) 82 | 83 | MainWindow.setCentralWidget(self.centralwidget) 84 | self.menubar = QMenuBar(MainWindow) 85 | self.menubar.setObjectName(u"menubar") 86 | self.menubar.setGeometry(QRect(0, 0, 1287, 22)) 87 | MainWindow.setMenuBar(self.menubar) 88 | self.statusbar = QStatusBar(MainWindow) 89 | self.statusbar.setObjectName(u"statusbar") 90 | MainWindow.setStatusBar(self.statusbar) 91 | 92 | self.retranslateUi(MainWindow) 93 | 94 | QMetaObject.connectSlotsByName(MainWindow) 95 | # setupUi 96 | 97 | def retranslateUi(self, MainWindow): 98 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) 99 | self.collapsibleGroupBox.setTitle(QCoreApplication.translate("MainWindow", u"CollapsibleGroupBox", None)) 100 | self.label.setText(QCoreApplication.translate("MainWindow", u"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore", None)) 101 | self.label_2.setText(QCoreApplication.translate("MainWindow", u"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat", None)) 102 | self.label_3.setText(QCoreApplication.translate("MainWindow", u"Pandas Model/TableView:", None)) 103 | # retranslateUi 104 | 105 | -------------------------------------------------------------------------------- /pyside6_utils/utility/__init__.py: -------------------------------------------------------------------------------- 1 | """Make all utilities importable using .utility.""" 2 | from .catch_show_exception_in_popup_decorator import catch_show_exception_in_popup_decorator 3 | from .signal_blocker import SignalBlocker -------------------------------------------------------------------------------- /pyside6_utils/utility/catch_show_exception_in_popup_decorator.py: -------------------------------------------------------------------------------- 1 | """Implements a decorator to be used in UI-classes to catch exceptions and show them in a message box""" 2 | import logging 3 | import traceback 4 | import typing 5 | 6 | from PySide6 import QtWidgets 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | def catch_show_exception_in_popup_decorator( 12 | func : typing.Callable, 13 | re_raise : bool = True, 14 | add_traceback_to_details : bool = True, 15 | custom_error_msg : str | None = None 16 | ): 17 | """Decorator that catches ALL exceptions and logs them. Also shows a message box if the app is running. 18 | TODO: second argument with list of exceptions to catch? 19 | Args: 20 | func (callable): The function which should be called 21 | re_raise (bool, optional): If True, the exception will be re-raised after being logged. Defaults to True. 22 | 23 | """ 24 | 25 | def catch_show_exception_in_popup(*args, **kwargs): 26 | try: 27 | return func(*args, **kwargs) 28 | except Exception as exception: #pylint: disable=broad-except 29 | log.exception(f"Exception in {func.__name__}: {exception}") 30 | #Also create a message box 31 | #Check if app is running, if so -> show message box 32 | if QtWidgets.QApplication.instance() is not None: 33 | msg = QtWidgets.QMessageBox() 34 | msg.setWindowTitle("Error") 35 | msg.setIcon(QtWidgets.QMessageBox.Icon.Critical) 36 | if custom_error_msg is not None: 37 | msg.setText(custom_error_msg) 38 | else: 39 | msg.setText(f"An error occured in function call to: {func.__name__}") 40 | msg.setInformativeText(f"{type(exception).__name__}: {exception}") 41 | trace_msg = f"Traceback:\n{traceback.format_exc()}" 42 | if add_traceback_to_details: 43 | msg.setDetailedText(trace_msg) 44 | msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) 45 | msg.exec() 46 | if re_raise: 47 | raise 48 | return catch_show_exception_in_popup 49 | -------------------------------------------------------------------------------- /pyside6_utils/utility/signal_blocker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements a signal blocker which can be used to temporarily block qt signals 3 | of a qt object using the with statement. 4 | """ 5 | 6 | from PySide6 import QtCore 7 | 8 | class SignalBlocker(QtCore.QObject): 9 | """Implements a signal blocker which can be used to temporarily block qt signals 10 | of a qt object using the with statement. 11 | """ 12 | def __init__(self, qt_object : QtCore.QObject) -> None: 13 | super().__init__() 14 | self._qt_object = qt_object 15 | 16 | def __enter__(self): 17 | self._qt_object.blockSignals(True) 18 | 19 | def __exit__(self, exc_type, exc_value, traceback): 20 | self._qt_object.blockSignals(False) 21 | -------------------------------------------------------------------------------- /pyside6_utils/utility/utility_functions.py: -------------------------------------------------------------------------------- 1 | """Implements small utility functions that do not fit anywhere else""" 2 | import re 3 | 4 | def snakecase(name : str): 5 | """Converts the passed tring to snakecase 6 | NOTE that _ is kept if already present in the string 7 | """ 8 | return re.sub(r"(\w)([A-Z])", r"\1_\2", name).lower() 9 | -------------------------------------------------------------------------------- /pyside6_utils/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | """Make all widgets importable using .widgets.""" 2 | from pyside6_utils.widgets.collapsible_group_box import CollapsibleGroupBox 3 | from pyside6_utils.widgets.console_widget import ConsoleWidget 4 | from pyside6_utils.widgets.dataclass_tree_view import DataClassTreeView 5 | from pyside6_utils.widgets.extended_mdi_area import ExtendedMdiArea 6 | from pyside6_utils.widgets.file_explorer_view import FileExplorerView 7 | from pyside6_utils.widgets.frameless_mdi_window import FramelessMdiWindow 8 | from pyside6_utils.widgets.overlay_widget import OverlayWidget 9 | from pyside6_utils.widgets.pandas_table_view import PandasTableView 10 | from pyside6_utils.widgets.range_selector import RangeSelector 11 | from pyside6_utils.widgets.square_frame import SquareFrame 12 | from pyside6_utils.widgets.widget_list import WidgetList 13 | -------------------------------------------------------------------------------- /pyside6_utils/widgets/collapsible_group_box.py: -------------------------------------------------------------------------------- 1 | """Implements a collapsible QGroupBox (using checkmark to toggle)""" 2 | import copy 3 | import logging 4 | 5 | from PySide6 import QtWidgets 6 | from PySide6.QtCore import Property 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | class CollapsibleGroupBox(QtWidgets.QGroupBox): 11 | """A collapsible QGroupBox (using checkmark to toggle)""" 12 | 13 | DESCRIPTION = "A collapsible QGroupBox (using checkmark to toggle)" 14 | 15 | #Use C++ style method names for functions to match Qt's naming convention 16 | def __init__(self, 17 | parent : QtWidgets.QWidget | None = None, 18 | title : str | None = None, 19 | *args, 20 | **kwargs 21 | ): #pylint: disable=keyword-arg-before-vararg 22 | super().__init__(parent=parent, *args, **kwargs) 23 | self.setCheckable(True) 24 | self._original_size_policy = self.sizePolicy() 25 | self._original_min_size = self.minimumSize() 26 | self._original_contents_margins = self.contentsMargins() 27 | self._original_flat_state = self.isFlat() 28 | self._original_size = self.size() 29 | self._collapses_horizontally = False 30 | self._collapses_vertically = True 31 | self._make_flat_when_collapsed = True 32 | self._collapsed = None 33 | 34 | if title is not None: 35 | self.setTitle(title) 36 | self.toggled.connect(self.toggle_active_collapse) 37 | 38 | def set_collapses_horizontally(self, collapse : bool): 39 | """Set whether the widget collapses horizontally when collapsed""" 40 | self._collapses_horizontally = collapse 41 | self.toggle_active_collapse(self.isChecked()) 42 | 43 | def get_collapses_horizontally(self): 44 | """Returns whether the widget collapses horizontally when collapsed""" 45 | return self._collapses_horizontally 46 | 47 | def set_collapses_vertically(self, collapse : bool): 48 | """Set whether the widget collapses vertically when collapsed""" 49 | self._collapses_vertically = collapse 50 | self.toggle_active_collapse(self.isChecked()) 51 | 52 | def get_collapses_vertically(self): 53 | """Returns whether the widget collapses vertically when collapsed""" 54 | return self._collapses_vertically 55 | 56 | def set_make_flat_when_collapsed(self, collapse : bool): 57 | """Set whether the widget should be flat when collapsed""" 58 | self._make_flat_when_collapsed = collapse 59 | self.toggle_active_collapse(self.isChecked()) 60 | 61 | def get_make_flat_when_collapsed(self): 62 | """Returns whether the widget is flat when collapsed""" 63 | return self._make_flat_when_collapsed 64 | 65 | def toggle_active_collapse(self, checked : bool): 66 | """Toggle the collapse-mode of the widget, based on the passed checked state""" 67 | if self._collapsed and checked != self._collapsed: 68 | return 69 | if self._collapsed is None or (self._collapsed != checked and not self._collapsed): #If initializing or 70 | # toggling to collapsed state, save the original values 71 | self._original_min_size = self.minimumSize() 72 | self._original_size_policy = self.sizePolicy() 73 | self._original_contents_margins = self.contentsMargins() 74 | self._original_flat_state = self.isFlat() 75 | self._original_size = self.size() 76 | 77 | new_size_policy = copy.copy(self._original_size_policy) #Make copies as not to modify originals 78 | # NOTE: we revert to original size, min_size and size_policy when toggling, even when these properties 79 | # are edited during runtime 80 | new_min_size = copy.copy(self._original_min_size) 81 | new_size = copy.copy(self._original_size) 82 | 83 | if self.makeFlatWhenCollapsed: 84 | if not checked: 85 | self.setFlat(True) 86 | else: 87 | self.setFlat(self._original_flat_state) 88 | 89 | for child in self.children(): #Hide/unhide all children 90 | if isinstance(child, QtWidgets.QWidget): 91 | child.setVisible(checked) 92 | 93 | if not checked: #No margins when collapsed 94 | self.setContentsMargins(0,0,0,0) 95 | else: 96 | self.setContentsMargins(self._original_contents_margins) 97 | 98 | if self.collapsesHorizontal: 99 | if not checked: 100 | new_size_policy.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Fixed) 101 | new_min_size.setWidth(0) 102 | new_size.setWidth(30) 103 | 104 | if self.collapsesVertical: 105 | if not checked: 106 | new_size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Fixed) 107 | new_min_size.setHeight(20) 108 | new_size.setHeight(0) 109 | 110 | self.setMinimumSize(new_min_size) 111 | self.resize(new_size) 112 | self.setSizePolicy(new_size_policy) 113 | self.updateGeometry() 114 | 115 | self._collapsed = not checked #Update the collapsed state 116 | 117 | #Use C++ style properties for matching Qt-Designer style 118 | collapsesHorizontal = Property(bool, get_collapses_horizontally, set_collapses_horizontally) 119 | collapsesVertical = Property(bool, get_collapses_vertically, set_collapses_vertically) 120 | makeFlatWhenCollapsed = Property(bool, get_make_flat_when_collapsed, set_make_flat_when_collapsed) 121 | 122 | 123 | 124 | def run_example_app(): 125 | """Creates a qt-app instance and runs the example""" 126 | log.debug("Now running collapsible groupbox example...") 127 | app = QtWidgets.QApplication([]) 128 | window = QtWidgets.QWidget() 129 | widget = CollapsibleGroupBox(title="Test") 130 | widget.setLayout(QtWidgets.QVBoxLayout()) 131 | widget.layout().addWidget(QtWidgets.QLabel("Test label1")) 132 | widget.layout().addWidget(QtWidgets.QLabel("Test label2")) 133 | widget.setTitle("Test title") 134 | window.setLayout(QtWidgets.QVBoxLayout()) 135 | window.layout().addWidget(widget) 136 | widget.show() 137 | window.show() 138 | app.exec() 139 | log.debug("Done!") 140 | 141 | 142 | 143 | if __name__ == "__main__": 144 | formatter = logging.Formatter("[{pathname:>90s}:{lineno:<4}] {levelname:<7s} {message}", style='{') 145 | handler = logging.StreamHandler() 146 | handler.setFormatter(formatter) 147 | logging.basicConfig( 148 | handlers=[handler], 149 | level=logging.DEBUG) #Without time 150 | 151 | #Run example 152 | run_example_app() 153 | -------------------------------------------------------------------------------- /pyside6_utils/widgets/dataclass_tree_view.py: -------------------------------------------------------------------------------- 1 | """ 2 | Treeview meant to be used with a dataclass-model, adds some extra functionality 3 | NOTE: since this wrapper is VERY simple, it is not included in the registrars 4 | """ 5 | 6 | 7 | import logging 8 | import typing 9 | 10 | from PySide6 import QtCore, QtGui, QtWidgets 11 | from pyside6_utils.models.dataclass_model import (DataclassModel, 12 | HasNoDefaultError) 13 | from pyside6_utils.models.dataclass_tree_item import DataclassTreeItem 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | class DataClassTreeView(QtWidgets.QTreeView): 18 | """ 19 | Treeview meant to be used with a dataclass-model, adds some extra functionality 20 | Extra functionality: 21 | - Custom contex-menu (right-click) to set back to default 22 | """ 23 | 24 | def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: 25 | super().__init__(parent) 26 | 27 | #Keep a list of expanded items (by node.name, re-expand them on model reset) 28 | self._expanded_items = set({}) 29 | #On expand, add the node.name to the list of expanded items 30 | self.expanded.connect(lambda index, expanded=True: self._on_expansion_change(index, expanded)) 31 | self.collapsed.connect(lambda index, expanded=False: self._on_expansion_change(index, expanded)) 32 | 33 | self._model_signals : typing.List[QtCore.SignalInstance] = [] 34 | 35 | #Context menu 36 | self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) 37 | self.customContextMenuRequested.connect(self.reset_expansion_state) 38 | 39 | def _on_expansion_change(self, index : QtCore.QModelIndex, expanded : bool) -> None: 40 | """ 41 | Keep track of the expanded items (by node.name) 42 | If we switch to a different model, we want to keep the expanded items the same 43 | """ 44 | attr_name = index.data(DataclassModel.CustomDataRoles.AttributeNameRole) 45 | if expanded and attr_name not in self._expanded_items: 46 | self._expanded_items.add(attr_name) 47 | elif not expanded and attr_name in self._expanded_items: 48 | self._expanded_items.remove(attr_name) 49 | # log.debug(f"Expansion changed: {index.data()} = {expanded}, expanded items: {self._expanded_items}") 50 | 51 | def _get_set_expansion_state(self, index : QtCore.QModelIndex) -> None: 52 | """Recursively sets the expansion state of the given index (and its children) using the current settings""" 53 | 54 | try: 55 | if index is None or not index.isValid(): 56 | return 57 | 58 | tree_item : DataclassTreeItem = index.data(DataclassModel.CustomDataRoles.TreeItemRole) 59 | if tree_item.name in self._expanded_items: 60 | self.expand(index) 61 | else: 62 | self.collapse(index) 63 | 64 | for child_nr in range(tree_item.child_count()): 65 | child = tree_item.child(child_nr) 66 | child_index = self.model().index(child.row(), 0, index) 67 | self._get_set_expansion_state(child_index) 68 | # child_index = self.model().createIndex(child.row(), 0, child) 69 | # self._get_set_expansion_state(child_index) 70 | except Exception as exception: #pylint: disable=broad-except 71 | log.exception(f"Error when setting expansion state {type(exception).__name__}: {exception}") 72 | 73 | def reset_expansion_state(self) -> None: 74 | """Resets the expansion state of all items in the treeview""" 75 | self.blockSignals(True) 76 | # self._get_set_expansion_state(self.model().index(0, 0).parent()) 77 | # self._get_set_expansion_state(self.rootIndex()) 78 | root = self.rootIndex() 79 | for child_nr in range(self.model().rowCount(root)): #Get all top-level items 80 | index = self.model().index(child_nr, 0, root) 81 | self._get_set_expansion_state(index) 82 | self.blockSignals(False) 83 | 84 | def setModel(self, model: QtCore.QAbstractItemModel | None) -> None: 85 | super().setModel(model) #type: ignore #NOTE: before connections, otherwise expand goes wrong bc view is not set 86 | if len(self._model_signals) > 0: #Disconnect all previous signals 87 | for signal in self._model_signals: 88 | self.disconnect(signal) #type: ignore 89 | self._model_signals = [] 90 | 91 | if model is not None: 92 | self._model_signals.append( #On reset -> re-set all expansion states according to the _expanded_items 93 | model.modelReset.connect(self.reset_expansion_state) #type: ignore 94 | ) 95 | #TODO: also do the same (selectively) on rowsInserted and rowsRemoved? 96 | 97 | 98 | def mousePressEvent(self, event: QtGui.QMouseEvent) -> bool: 99 | #Mark event as accepted, so the selection is not changed 100 | # event.accept() 101 | if not event.button() == QtCore.Qt.MouseButton.RightButton: 102 | return super().mousePressEvent(event) #type: ignore 103 | pos = event.pos() 104 | index = self.indexAt(pos) 105 | if not index.isValid(): 106 | return super().mousePressEvent(event) #type: ignore 107 | return self._try_context_menu_event(pos) 108 | 109 | 110 | def _try_context_menu_event(self, pos : QtCore.QPoint) -> bool: 111 | """Overridden context menu event to add a custom context menu""" 112 | menu = QtWidgets.QMenu(self) 113 | index = self.indexAt(pos) 114 | if not index.isValid(): 115 | return False 116 | 117 | # cur_field = index.data(DataclassModel.CustomDataRoles.FieldRole) 118 | cur_data = index.data(QtCore.Qt.ItemDataRole.EditRole) 119 | # if not isinstance(cur_field, Field): 120 | # log.debug(f"Selected item is not a field: {cur_field}") 121 | # return False 122 | 123 | # if cur_field is None: 124 | # return False 125 | 126 | # if hasattr(cur_field, "default"): 127 | # default_val = cur_field.default 128 | try: 129 | default_val = index.data(DataclassModel.CustomDataRoles.DefaultValueRole) 130 | if default_val != cur_data: 131 | menu.addAction(f"Set to default ({default_val})", lambda x=index: self.set_index_to_default(index)) 132 | else: 133 | menu.addAction("Set to default (unchanged)", None) 134 | except HasNoDefaultError: 135 | menu.addAction("Set to default (unchanged)", None) 136 | 137 | 138 | menu.exec(self.mapToGlobal(pos)) 139 | return True 140 | 141 | def set_index_to_default(self, index : QtCore.QModelIndex) -> None: 142 | """Sets the selected item to default""" 143 | if not index.isValid(): 144 | return 145 | try: 146 | model = self.model() 147 | model.setData(index, None, DataclassModel.CustomDataRoles.DefaultValueRole) #Set default value (found internally) 148 | except Exception as exception: 149 | log.exception(f"Error when setting to default {type(exception).__name__}: {exception}") 150 | raise exception 151 | 152 | def run_example_app(): 153 | """Run an example using ./examples/example_dataclass.py and a dataclass treeview & model 154 | As well as a tableview with the same model. Note that the tableview does not support nested dataclass-attributes. 155 | """ 156 | #pylint: disable=import-outside-toplevel 157 | import sys 158 | 159 | from pyside6_utils.examples.example_dataclass import ExampleDataClass 160 | from pyside6_utils.widgets.delegates.dataclass_editors_delegate import \ 161 | DataclassEditorsDelegate 162 | 163 | app = QtWidgets.QApplication() 164 | test_data = ExampleDataClass() 165 | model = DataclassModel(test_data) 166 | view1 = DataClassTreeView() 167 | view2= QtWidgets.QTableView() 168 | 169 | view1.setModel(model) 170 | view1.setItemDelegate(DataclassEditorsDelegate()) 171 | view2.setModel(model) 172 | view2.setItemDelegate(DataclassEditorsDelegate()) 173 | #adjust treeview to fit contents, but allow user to resize 174 | #Fit header of view 1 to contents, then allow user to resize 175 | view1.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents) 176 | view1.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) 177 | view1.show() 178 | view2.show() 179 | 180 | #Set window size to 400 and display 181 | view1.resize(400, 400) 182 | view2.resize(400, 400) 183 | 184 | #Place windows next to each other 185 | view1.move(1000, 400) 186 | view2.move(1400, 400) 187 | app.exec() 188 | print(test_data) 189 | sys.exit() 190 | 191 | 192 | 193 | if __name__ == "__main__": 194 | formatter = logging.Formatter("[{pathname:>90s}:{lineno:<4}] {levelname:<7s} {message}", style='{') 195 | handler = logging.StreamHandler() 196 | handler.setFormatter(formatter) 197 | logging.basicConfig( 198 | handlers=[handler], 199 | level=logging.DEBUG) #Without time 200 | 201 | #Run example 202 | run_example_app() -------------------------------------------------------------------------------- /pyside6_utils/widgets/delegates/__init__.py: -------------------------------------------------------------------------------- 1 | """Make all delegates importable using .widgets.delegates.""" 2 | #pylint: disable=unused-import 3 | from pyside6_utils.widgets.delegates.console_widget_delegate import ConsoleWidgetDelegate 4 | from pyside6_utils.widgets.delegates.dataclass_editors_delegate import DataclassEditorsDelegate 5 | -------------------------------------------------------------------------------- /pyside6_utils/widgets/delegates/console_widget_delegate.py: -------------------------------------------------------------------------------- 1 | """Implements a delegate that adds a "delete" button ("x") to the console widget treeview""" 2 | from PySide6 import QtCore, QtGui, QtWidgets 3 | 4 | class ConsoleWidgetDelegate(QtWidgets.QStyledItemDelegate): 5 | """Custom delegate that implements an "x" button 6 | """ 7 | 8 | deleteHoverItem = QtCore.Signal(QtCore.QModelIndex) #Emitted index when the "delete" button is clicked on an item 9 | 10 | #Custom delegate that puts an x-button at the end of the first column of the file-selection treeview 11 | #When this button is clicked, the file is deleted 12 | def __init__(self, parent : QtWidgets.QWidget | None = None) -> None: 13 | super().__init__(parent) 14 | # self._delete_button.setFixedSize(16, 16) 15 | 16 | #Get the icon size (normal) 17 | self.icon_size = QtWidgets.QApplication.style().pixelMetric(QtWidgets.QStyle.PixelMetric.PM_LargeIconSize) 18 | 19 | #Create pixmap from ":/Icons/places/user-trash.png" 20 | self.trash_icon = QtWidgets.QApplication.style().standardIcon( 21 | QtWidgets.QStyle.StandardPixmap.SP_TitleBarCloseButton) 22 | 23 | # self.trash_icon_pixmap = QtGui.QPixmap(":/Icons/places/user-trash.png") 24 | self.hovering_del_btn = False 25 | 26 | 27 | def paint(self, 28 | painter : QtGui.QPainter, 29 | option : QtWidgets.QStyleOptionViewItem, #TODO: typehinting not working? rect, state and widget not found 30 | index : QtCore.QModelIndex | QtCore.QPersistentModelIndex 31 | ) -> None: 32 | #First draw the default item 33 | super().paint(painter, option, index) 34 | 35 | option_state : QtWidgets.QStyle.StateFlag = option.state #type: ignore 36 | option_rect : QtCore.QRect = option.rect #type: ignore 37 | 38 | #Set icon size based on the height of the item 39 | self.icon_size = option_rect.height() - 10 #10 is the padding on both top and bottom 40 | 41 | if option_state & \ 42 | (QtWidgets.QStyle.StateFlag.State_MouseOver | (QtWidgets.QStyle.StateFlag.State_Selected)): 43 | #If mouse-over event is detected or part of selection #TODO: active? 44 | painter.save() 45 | #Get the rect of the first column 46 | icon_rect = QtCore.QRect( 47 | option_rect.right() - self.icon_size, 48 | option_rect.top() + (option_rect.height()-self.icon_size)//2, 49 | self.icon_size, 50 | self.icon_size 51 | ) 52 | option_widget : QtWidgets.QWidget = option.widget #type: ignore 53 | mouse_pos = option_widget.mapFromGlobal(QtGui.QCursor.pos()) 54 | if icon_rect.contains(mouse_pos): 55 | painter.fillRect(icon_rect, QtGui.QColor(0, 0, 0, 150)) 56 | 57 | QtGui.QIcon.paint(self.trash_icon, 58 | painter, 59 | icon_rect, 60 | mode=QtGui.QIcon.Mode.Normal, 61 | state=QtGui.QIcon.State.On 62 | ) 63 | 64 | 65 | #Restore the painter state 66 | painter.restore() 67 | 68 | 69 | def editorEvent(self, 70 | event : QtCore.QEvent, 71 | model : QtCore.QAbstractItemModel, 72 | option : QtWidgets.QStyleOptionViewItem, #TODO: typehinting not working? rect, state and widget not found 73 | index : QtCore.QModelIndex) -> bool: 74 | 75 | event_pos = event.position() #Local position #type: ignore 76 | global_pos = event_pos.toPoint() 77 | option_widget : QtWidgets.QTreeWidget = option.widget #type: ignore #TODO: option typehinting not working? 78 | option_rect : QtCore.QRect = option.rect #type: ignore 79 | 80 | 81 | 82 | #Check if the user clicked on the icon 83 | if event.type() == QtCore.QEvent.Type.MouseButtonPress: 84 | #Get the rect of the first column 85 | option_rect : QtCore.QRect = option.rect #type: ignore 86 | #Get the icon rect 87 | icon_rect = QtCore.QRect(option_rect.right() - self.icon_size, option_rect.top(), self.icon_size, self.icon_size) 88 | #Check if the mouse is inside the icon rect 89 | if icon_rect.contains(global_pos): 90 | #Emit the delete signal 91 | self.deleteHoverItem.emit(index) 92 | #Block event propagation 93 | event.setAccepted(True) 94 | return True 95 | 96 | elif event.type() == QtCore.QEvent.Type.MouseMove: 97 | #Get the rect of the first column 98 | #Get the icon rect 99 | icon_rect = QtCore.QRect( 100 | option_rect.right() - self.icon_size, option_rect.top() + (option_rect.height()-self.icon_size)//2, 101 | self.icon_size, 102 | self.icon_size 103 | ) 104 | #Check if the mouse is inside the icon rect 105 | if icon_rect.contains(global_pos): 106 | if not self.hovering_del_btn: #Only update view if state changed 107 | self.hovering_del_btn = True 108 | option_widget.viewport().update() 109 | return True #Make sure that mousemove does not selet the underlying item 110 | elif self.hovering_del_btn: 111 | self.hovering_del_btn = False 112 | option_widget.viewport().update() 113 | 114 | return super().editorEvent(event, model, option, index) 115 | -------------------------------------------------------------------------------- /pyside6_utils/widgets/extended_mdi_area.py: -------------------------------------------------------------------------------- 1 | """Implements a QMdiArea with some extra functionality.""" 2 | import enum 3 | import logging 4 | from typing import List 5 | 6 | from PySide6 import QtCore, QtGui, QtWidgets 7 | 8 | import pyside6_utils.widgets.frameless_mdi_window as frameless_mdi_window 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | class ExtendedMdiArea(QtWidgets.QMdiArea): 13 | """Implements a QMdiArea with some extra functionality. 14 | E.g.: 15 | -Zooming in/out on a single panel 16 | -Context menu for sorting, un-minimizing, and toggling tabbed view 17 | 18 | If the added windows are FramelessMdiWindows, some extra functionality is added: 19 | -Disable zoom/minimize/titlebar when tabbified 20 | 21 | TODO: save/load window layout? (e.g. when closing/opening the app) 22 | TODO: autoscale subwindows when resizing the mdi area - even when not tiled 23 | """ 24 | DESCRIPTION = "Implements a QMdiArea with some extra functionality." 25 | 26 | class DisplayMode(enum.Enum): 27 | """Enum for the display mode of the mdi area. 28 | """ 29 | TILED = enum.auto() 30 | CASCADED = enum.auto() 31 | #TODO: zoomed? 32 | 33 | def __init__(self, 34 | parent: QtWidgets.QWidget | None = None, 35 | re_tile_cascade_on_subwindow_changes : bool = True #Retile the subwindows when a subwindow is added/removed 36 | ) -> None: 37 | super().__init__(parent) 38 | self._subwindows : list[QtWidgets.QMdiSubWindow] = [] 39 | self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) 40 | self.customContextMenuRequested.connect(self.context_menu_requested) 41 | 42 | self._cur_display_method = ExtendedMdiArea.DisplayMode.TILED #By default, tile the subwindows 43 | self._re_tile_cascade_on_subwindow_changes = re_tile_cascade_on_subwindow_changes 44 | 45 | self.setActivationOrder(QtWidgets.QMdiArea.WindowOrder.StackingOrder) #Set the activation order to creation order 46 | 47 | 48 | def order_windows_by_titles(self, titles : List[str]) -> None: 49 | """Orders the subwindows by the given titles. 50 | 51 | Args: 52 | titles (List[str]): The titles to order the subwindows by 53 | """ 54 | #Sort the subwindows by the given titles 55 | self._subwindows.sort(key = lambda subwindow: titles.index(subwindow.windowTitle())) 56 | 57 | 58 | def order_windows_by_windowlist(self, windowlist : List[QtWidgets.QMdiSubWindow]) -> None: 59 | """Orders the subwindows by the given windowlist. If a subwindow is not in the windowlist, it is moved to the end. 60 | 61 | Args: 62 | windowlist (List[QtWidgets.QMdiSubWindow]): The windowlist to order the subwindows by 63 | """ 64 | #Sort the subwindows by the given windowlist 65 | self._subwindows.sort( 66 | key = lambda subwindow: windowlist.index(subwindow) if subwindow in windowlist else len(windowlist) 67 | ) 68 | 69 | def addSubWindow(self, 70 | widget: QtWidgets.QWidget, 71 | flags: QtCore.Qt.WindowType = QtCore.Qt.WindowType() #type:ignore 72 | ) -> QtWidgets.QMdiSubWindow: 73 | """Adds a subwindow to the mdi area. 74 | """ 75 | subwindow = super().addSubWindow(widget, flags) 76 | self._subwindows.append(subwindow) #Keep track of window 77 | subwindow.show() 78 | if self._cur_display_method == ExtendedMdiArea.DisplayMode.TILED\ 79 | and self._re_tile_cascade_on_subwindow_changes: #If tiled -> retile, nothing if tabed/cascaded. 80 | self.tileSubWindows() 81 | 82 | return subwindow 83 | 84 | def removeSubWindow(self, widget: QtWidgets.QWidget) -> None: 85 | self._subwindows.remove(widget) #Keep track of window #type:ignore 86 | if self._cur_display_method == ExtendedMdiArea.DisplayMode.TILED and self._re_tile_cascade_on_subwindow_changes: 87 | self.tileSubWindows() 88 | return super().removeSubWindow(widget) 89 | 90 | 91 | def set_tabbified(self, tabbified : bool) -> None: 92 | """Set the tabbified state of the mdi area. 93 | Args: 94 | tabbified (bool): Whether the mdi area should be tabbified 95 | """ 96 | if self.viewMode() == QtWidgets.QMdiArea.ViewMode.TabbedView and tabbified or\ 97 | self.viewMode() == QtWidgets.QMdiArea.ViewMode.SubWindowView and not tabbified: 98 | #If no change -> continue 99 | return 100 | 101 | if tabbified: 102 | self.setViewMode(QtWidgets.QMdiArea.ViewMode.TabbedView) 103 | else: 104 | self.setViewMode(QtWidgets.QMdiArea.ViewMode.SubWindowView) 105 | 106 | for window in self._subwindows: 107 | if isinstance(window, frameless_mdi_window.FramelessMdiWindow): 108 | window.set_tabbed_mode(tabbified) 109 | 110 | def is_tabbified(self)-> bool: 111 | """Returns whether the mdi area is currently tabbified""" 112 | return self.viewMode() == QtWidgets.QMdiArea.ViewMode.TabbedView 113 | 114 | def toggle_tabbified(self) -> None: 115 | """Toggles the tabbed view of the mdi area. 116 | """ 117 | self.set_tabbified(not self.is_tabbified()) 118 | 119 | def tileSubWindows(self) -> None: 120 | """Tiles the subwindows, also untabs windows if current state is tabbed""" 121 | self.set_tabbified(False) 122 | self._cur_display_method = ExtendedMdiArea.DisplayMode.TILED 123 | for window in self._subwindows[::-1]: 124 | #To get the appropriate order, we activate the window, and then tile the subwindows 125 | self.setActiveSubWindow(window) 126 | 127 | return super().tileSubWindows() 128 | 129 | def cascadeSubWindows(self) -> None: 130 | """Cascades the subwindows, also untabs windows if current state is tabbed""" 131 | self.set_tabbified(False) 132 | self._cur_display_method = ExtendedMdiArea.DisplayMode.CASCADED 133 | return super().cascadeSubWindows() 134 | 135 | def add_actions_to_menu(self, menu : QtWidgets.QMenu) -> None: 136 | """Adds all available actions of this widget to the given menu. 137 | """ 138 | tile_submenu = menu.addMenu("Tile-mode") 139 | tile_submenu.setIcon(QtGui.QIcon(":/icons/apps/preferences-system-windows.png")) 140 | tile_submenu.addAction("Tile", self.tileSubWindows) 141 | tile_submenu.addAction("Cascade", self.cascadeSubWindows) 142 | show_hidden = menu.addMenu("Un-minimize") 143 | show_hidden.setIcon(QtGui.QIcon(":/icons/actions/list-add.png")) 144 | for subwindow in self._subwindows: 145 | if subwindow.isMinimized(): 146 | show_hidden.addAction(subwindow.windowTitle(), subwindow.showNormal) 147 | if show_hidden.isEmpty(): 148 | show_hidden.setEnabled(False) 149 | 150 | bring_to_front = menu.addMenu("Bring to front") 151 | bring_to_front.setIcon(QtGui.QIcon(":/icons/actions/go-up.png")) 152 | for subwindow in self._subwindows: 153 | bring_to_front.addAction(subwindow.windowTitle(), lambda subwindow=subwindow: self.setActiveSubWindow(subwindow)) 154 | 155 | tab_action = menu.addAction("Toggle Tabbed View") 156 | tab_action.triggered.connect(self.toggle_tabbified) 157 | tab_action.setIcon(QtGui.QIcon(":/icons/actions/tab-new.png")) 158 | 159 | 160 | def context_menu_requested(self, pos: QtCore.QPoint) -> None: 161 | """Shows a context menu with options at the given position. 162 | """ 163 | menu = QtWidgets.QMenu() 164 | self.add_actions_to_menu(menu) 165 | menu.exec(self.mapToGlobal(pos)) 166 | 167 | 168 | def run_example_app(): 169 | """Creates a qt-app instance and runs the example""" 170 | import sys # pylint: disable=import-outside-toplevel 171 | log.info(f"Running example app for {ExtendedMdiArea.__name__}...") 172 | app = QtWidgets.QApplication(sys.argv) 173 | area = ExtendedMdiArea() 174 | area.show() 175 | 176 | mdi_windows = [] 177 | 178 | for i in range(6): 179 | mdi_window = frameless_mdi_window.FramelessMdiWindow() 180 | mdi_window.setWindowTitle(f"Test Window {i+1}") 181 | label = QtWidgets.QLabel(f"Test Window {i+1}") 182 | label.setFont(QtGui.QFont("Arial", 20)) 183 | mdi_window.setWidget(label) 184 | area.addSubWindow(mdi_window) 185 | mdi_window.show() 186 | mdi_windows.append(mdi_window) 187 | 188 | area.tileSubWindows() 189 | sys.exit(app.exec()) 190 | 191 | if __name__ == "__main__": 192 | formatter = logging.Formatter("[{pathname:>90s}:{lineno:<4}] {levelname:<7s} {message}", style='{') 193 | handler = logging.StreamHandler() 194 | handler.setFormatter(formatter) 195 | logging.basicConfig( 196 | handlers=[handler], 197 | level=logging.DEBUG) #Without time 198 | log.debug("Now running collapsible groupbox example...") 199 | run_example_app() 200 | -------------------------------------------------------------------------------- /pyside6_utils/widgets/overlay_widget.py: -------------------------------------------------------------------------------- 1 | """Implements an overlay widget that acts as a container but allows displaying another widget on top of it.""" 2 | 3 | import logging 4 | 5 | from PySide6 import QtCore, QtGui, QtWidgets 6 | from PySide6.QtCore import Qt 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | class OverlayWidget(QtWidgets.QWidget): 11 | """ 12 | Container-like widget which allows the user to overlay another widget on top of it. Switching on/off the overlay 13 | widget is done by setting the overlayHidden property. 14 | """ 15 | 16 | DESCRIPTION = "Basic QtWidget that allows displaying another widget on top of it using setOverlayWidget." 17 | 18 | 19 | def __init__(self, parent: QtWidgets.QWidget | None) -> None: 20 | super().__init__(parent) 21 | 22 | self._display_overlay = False 23 | self._overlay_widget = None 24 | self._overlay_widget_container: QtWidgets.QWidget = QtWidgets.QWidget(self) 25 | self._overlay_widget_container.setParent(self) 26 | self._overlay_widget_container.setWindowFlags(Qt.WindowType.Widget | Qt.WindowType.FramelessWindowHint) 27 | self._overlay_widget_container.setAutoFillBackground(True) 28 | self._overlay_widget_container.setContentsMargins(0, 0, 0, 0) 29 | self._overlay_widget_container.raise_() 30 | 31 | self._cur_background_color = None 32 | self.set_overlay_mouse_block(True) 33 | self.set_background_color(QtGui.QColor(200, 200, 200, 150)) 34 | 35 | 36 | def set_overlay_mouse_block(self, block: bool) -> None: 37 | """Sets whether the overlay widget should block mouse events from reaching the underlying widget.""" 38 | self._overlay_widget_container.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, not block) 39 | 40 | def get_overlay_mouse_block(self) -> bool: 41 | """Returns whether the overlay widget blocks mouse events from reaching the underlying widget.""" 42 | return self._overlay_widget_container.testAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) 43 | 44 | def showEvent(self, event: QtGui.QShowEvent) -> None: 45 | """On show, raise the overlay widget to make sure it is on top.""" 46 | self._overlay_widget_container.raise_() #Make sure the overlay widget is on top 47 | return super().showEvent(event) 48 | 49 | 50 | def set_overlay_widget(self, overlay_widget: QtWidgets.QWidget) -> None: 51 | """ 52 | Sets the overlay widget to display on top of this widget. 53 | """ 54 | self._overlay_widget = overlay_widget 55 | self._overlay_widget_container.setLayout(QtWidgets.QVBoxLayout()) #Reset the layout to remove any previous 56 | self._overlay_widget_container.layout().addWidget(overlay_widget) 57 | self._overlay_widget_container.resize(self.size()) 58 | self._overlay_widget_container.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) 59 | self._overlay_widget_container.raise_() 60 | 61 | 62 | def resizeEvent(self, event: QtGui.QResizeEvent) -> None: 63 | """ 64 | #On resize, update the overlay widget size 65 | """ 66 | super().resizeEvent(event) 67 | self._overlay_widget_container.resize(self.size()) 68 | 69 | def set_overlay_hidden(self, hidden: bool) -> None: 70 | """ 71 | Sets the overlay widget to be hidden or visible. 72 | """ 73 | self._overlay_widget_container.setHidden(hidden) 74 | 75 | def get_overlay_hidden(self) -> bool: 76 | """ 77 | Returns whether the overlay widget is hidden or visible. 78 | """ 79 | return self._overlay_widget_container.isHidden() 80 | 81 | 82 | def set_background_color(self, color: QtGui.QColor) -> None: 83 | """ 84 | Sets the background color of the overlay widget. 85 | """ 86 | self._cur_background_color = color 87 | style = QtWidgets.QApplication.style() 88 | palette = style.standardPalette() 89 | palette.setColor(QtGui.QPalette.ColorRole.Window, color) #Background color 90 | self._overlay_widget_container.setPalette(palette) 91 | def get_background_color(self) -> QtGui.QColor | None: 92 | """ 93 | Returns the background color of the overlay widget. 94 | """ 95 | return self._cur_background_color 96 | 97 | overlayHidden = QtCore.Property(bool, get_overlay_hidden, set_overlay_hidden) 98 | overlayBlocksMouse = QtCore.Property(bool, get_overlay_mouse_block, set_overlay_mouse_block) 99 | overlayBackgroundColor = QtCore.Property(QtGui.QColor, get_background_color, set_background_color) 100 | 101 | 102 | 103 | def run_example_app(): 104 | """Creates a qt-app instance and runs the example""" 105 | log.info(f"Running example app for {OverlayWidget.__name__}...") 106 | 107 | app = QtWidgets.QApplication([]) 108 | widget = OverlayWidget(None) 109 | 110 | #Some buttons for behind the overlay 111 | buttons = QtWidgets.QPushButton("Hello") 112 | buttons2 = QtWidgets.QPushButton("Hello2") 113 | buttons3 = QtWidgets.QPushButton("Hello3") 114 | 115 | buttons.setFixedSize(300, 100) 116 | buttons2.setFixedSize(300, 100) 117 | buttons3.setFixedSize(300, 100) 118 | widget.setLayout(QtWidgets.QVBoxLayout()) 119 | widget.layout().addWidget(buttons) 120 | widget.layout().addWidget(buttons2) 121 | widget.layout().addWidget(buttons3) 122 | 123 | #Create overlay and create button to hide it 124 | overlay = QtWidgets.QWidget(None) 125 | overlay.setFixedSize(300, 100) 126 | overlay.setLayout(QtWidgets.QVBoxLayout()) 127 | overlay.layout().addWidget(QtWidgets.QLabel("This is an overlay widget with a button to hide it")) 128 | btn = QtWidgets.QPushButton("Hide overlay") 129 | btn.clicked.connect(lambda: widget.set_overlay_hidden(True)) 130 | overlay.layout().addWidget(btn) 131 | widget.set_overlay_widget(overlay) 132 | widget.show() 133 | 134 | app.exec() 135 | log.info("Done!") 136 | 137 | 138 | if __name__ == "__main__": 139 | formatter = logging.Formatter("[{pathname:>90s}:{lineno:<4}] {levelname:<7s} {message}", style='{') 140 | handler = logging.StreamHandler() 141 | handler.setFormatter(formatter) 142 | logging.basicConfig( 143 | handlers=[handler], 144 | level=logging.DEBUG) #Without time 145 | log.info("Now running overlay widget example...") 146 | run_example_app() 147 | -------------------------------------------------------------------------------- /pyside6_utils/widgets/pandas_table_view.py: -------------------------------------------------------------------------------- 1 | """Implements a Qt-View used to display a pandas dataframe as a table. 2 | This view implements some extra funcitonality such as a custom proxy model to enable better sorting and filtering. 3 | Also enables the copy/pasting of data, while setting the status bar to display the number of selected cells, the average 4 | and the sum of the selected data. 5 | """ 6 | 7 | 8 | import logging 9 | import os 10 | import typing 11 | from enum import Enum 12 | 13 | import pandas as pd 14 | from PySide6 import QtCore 15 | from PySide6.QtCore import Qt 16 | from PySide6.QtGui import QKeySequence, QShortcut 17 | from PySide6.QtWidgets import QApplication, QTableView 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class TableViewRoles(Enum): 23 | """Enum woth roles used by the table view""" 24 | HEADER_ROLE = Qt.ItemDataRole.UserRole + 1 25 | 26 | # class FilterSortHeaderView(QHeaderView): 27 | # def __init__(self, orientation: QtCore.Qt.Orientation, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: 28 | # super().__init__(orientation, parent) 29 | 30 | class PandasTableProxyModel(QtCore.QSortFilterProxyModel): 31 | """ 32 | Enables sorting and filtering and special icons indicating which columns are sorted 33 | """ 34 | def __init__(self, parent=None): 35 | super().__init__(parent) 36 | 37 | self._sort_columns = [] #List of column names to sort by 38 | self._sort_order = [] #Qt.DescendingOrder or Qt.SortOrder.AscendingOrder 39 | self._filter_columns = [] #List of column names to filter by 40 | self._filter_strings = [] #List of strings to filter by 41 | # self.setSortRole(QtCore.Qt.ItemDataRole.EditRole) #Sort by the edit role 42 | # (so that we can sort by the value in the cell, not the display role) 43 | 44 | 45 | def headerData(self, 46 | section: int, 47 | orientation: QtCore.Qt.Orientation, 48 | role: int = Qt.ItemDataRole.DisplayRole 49 | ) -> typing.Any: 50 | if role == TableViewRoles.HEADER_ROLE.value: 51 | default_data = self.sourceModel().headerData(section, orientation, Qt.ItemDataRole.DisplayRole) 52 | # sort_by = None #None=not sorted, Qt.SortOrder.AscendingOrder=ascending, Qt.DescendingOrder=descending 53 | # #Check if this column is sorted 54 | # for i, col in enumerate(self._sort_columns): 55 | # if col == default_data: 56 | # sort_by = self._sort_order[i] 57 | # break 58 | 59 | return (*default_data,) 60 | 61 | return super().headerData(section, orientation, role) 62 | 63 | def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: 64 | """Sort by the edit role (so that we can sort by the value in the cell, not the display role)""" 65 | ldata = self.sourceModel().data(left, Qt.ItemDataRole.EditRole) 66 | rdata = self.sourceModel().data(right, Qt.ItemDataRole.EditRole) 67 | lnone = ldata is None or pd.isnull(ldata) 68 | rnone = rdata is None or pd.isnull(rdata) 69 | if lnone: 70 | if rnone: 71 | return False 72 | return True 73 | try: 74 | val = ldata < rdata 75 | return val 76 | except Exception as exception: #pylint: disable=broad-except,unused-variable 77 | return super().lessThan(left, right) 78 | 79 | 80 | 81 | class PandasTableView(QTableView): 82 | """A view to display a pandas dataframe, works best in combination with PandasTableModel - places a" 83 | proxymodel in between the tableview and the model to allow sorting and filtering""" 84 | DESCRIPTION = ("A view to display a pandas dataframe, works best in combination with PandasTableModel - places a" 85 | "proxymodel in between the tableview and the model to allow sorting and filtering") 86 | 87 | def __init__(self, parent=None, status_bar=None): 88 | QTableView.__init__(self, parent) 89 | self._status_bar = status_bar 90 | #If ctrl+c is pressed, copy the selection to the clipboard 91 | self._copy_shortcut = QShortcut(QKeySequence("Ctrl+C"), self) 92 | self._copy_shortcut.activated.connect(self.copy_selection_to_clipboard) 93 | 94 | 95 | self.proxy_model = PandasTableProxyModel(self) 96 | self.proxy_model.setDynamicSortFilter(True) 97 | self.proxy_model.setSourceModel(None) #type: ignore 98 | super().setModel(self.proxy_model) 99 | 100 | self.selectionModel().selectionChanged.connect(self.display_selection_stats) #TODO: 101 | self.setSortingEnabled(True) 102 | #Detect right-clicks on table headers 103 | # self.horizontalHeader().sectionClicked.connect(self.headerClicked) 104 | 105 | 106 | def set_status_bar(self, status_bar): 107 | """Set the status bar to be used by the view, e.g. when making a selection""" 108 | self._status_bar = status_bar 109 | 110 | def setModel(self, model: QtCore.QAbstractItemModel) -> None: #type: ignore 111 | """Set the model for the table view""" 112 | return self.proxy_model.setSourceModel(model) 113 | 114 | 115 | 116 | def copy_selection_to_clipboard(self): 117 | """Copy the current selection to the clipboard according to excel-like-format""" 118 | selected = self.selectedIndexes() 119 | rows = [] 120 | columns = [] 121 | # cycle all selected items to get the minimum row and column, so that the 122 | # reference will always be [0, 0] 123 | for index in selected: 124 | rows.append(index.row()) 125 | columns.append(index.column()) 126 | min_row = min(rows) 127 | max_row = max(rows) 128 | min_col = min(columns) 129 | max_col = max(columns) 130 | 131 | #Create a string with the selected data, using tabs and newlines (excel-like-format) 132 | clip_data = "" 133 | for row in range(min_row, max_row + 1): 134 | sep = "" 135 | for column in range(min_col, max_col + 1): 136 | clip_data += sep 137 | index = self.model().index(row, column) 138 | sep = "\t" 139 | clip_data += str(self.model().data(index, Qt.ItemDataRole.EditRole)) 140 | clip_data += os.linesep 141 | 142 | clipboard = QApplication.clipboard() 143 | clipboard.clear() 144 | clipboard.setText(clip_data) 145 | 146 | 147 | def get_selected_cells(self): 148 | """Return a list of the selected cells""" 149 | cells = [] 150 | for index in self.selectedIndexes(): 151 | cells.append((index.row(), index.column())) 152 | return cells 153 | 154 | def get_selected_data(self, discard_empty=True, discard_nan=True): 155 | """Return a list of the selected data""" 156 | data = [] 157 | for index in self.selectedIndexes(): 158 | if discard_empty and self.model().data(index, Qt.ItemDataRole.DisplayRole) == "": 159 | continue 160 | if discard_nan and self.model().data(index, Qt.ItemDataRole.EditRole) is None or\ 161 | pd.isnull(self.model().data(index, Qt.ItemDataRole.EditRole)): 162 | continue 163 | data.append(self.model().data(index, Qt.ItemDataRole.EditRole)) 164 | return data 165 | 166 | 167 | def display_selection_stats(self): 168 | """Display the number of selected cells, the average and the sum of the selected data""" 169 | if self._status_bar is None: #Only show stats if a status bar is available 170 | return 171 | 172 | #Get the data from the selected cells 173 | data = self.get_selected_data() 174 | 175 | #Get the average and sum of the selected data 176 | try: 177 | average = round(sum(data) / len(data), 2) 178 | total = round(sum(data), 2) 179 | thesum = sum(data) 180 | except (TypeError, ZeroDivisionError): 181 | average = "-" 182 | total = "-" 183 | thesum = "-" 184 | additional_text = "" 185 | 186 | if len(data) == 2: #If we selected exactly 2 cells -> also show the difference 187 | try: 188 | difference = abs(data[1] - data[0]) 189 | additional_text = f", Difference: {difference}" 190 | except (TypeError, ZeroDivisionError): 191 | additional_text = "" 192 | #Display the results 193 | self._status_bar.showMessage( 194 | f"Selected cells: {len(data)}, Average: {average}, Total: {total}, Sum: {thesum}{additional_text}" 195 | ) 196 | 197 | 198 | def run_example_app(): 199 | """Creates a qt-app instance and runs the example""" 200 | #pylint: disable=import-outside-toplevel 201 | from PySide6 import QtWidgets 202 | 203 | from pyside6_utils.models import PandasTableModel 204 | log.info(f"Running example app for {PandasTableView.__name__}...") 205 | 206 | app = QtWidgets.QApplication([]) 207 | test_window = QtWidgets.QMainWindow() 208 | #====== Example df for PandasTableView ====== 209 | example_df = pd.DataFrame({ 210 | "Column 1": [1, 2, 3, 4, 5], 211 | "Column 2": [10, 20, 30, 40, 50], 212 | "Column 3": [100, 200, 300, 400, 500], 213 | "Column 4": [1000, 2000, 3000, 4000, 5000], 214 | "Column 5": [10000, 20000, 30000, 40000, 50000], 215 | }) 216 | example_df_model = PandasTableModel(example_df) 217 | example_view = PandasTableView() 218 | example_view.setModel(example_df_model) 219 | example_view.set_status_bar(test_window.statusBar()) 220 | test_window.setCentralWidget(example_view) 221 | example_view.show() 222 | test_window.show() 223 | app.exec() 224 | 225 | 226 | 227 | 228 | 229 | if __name__ == "__main__": 230 | formatter = logging.Formatter("[{pathname:>90s}:{lineno:<4}] {levelname:<7s} {message}", style='{') 231 | handler = logging.StreamHandler() 232 | handler.setFormatter(formatter) 233 | logging.basicConfig( 234 | handlers=[handler], 235 | level=logging.DEBUG) #Without time 236 | 237 | #Run example 238 | run_example_app() -------------------------------------------------------------------------------- /pyside6_utils/widgets/square_frame.py: -------------------------------------------------------------------------------- 1 | """Implements a wrapper that forces the inside widget to remain square""" 2 | import logging 3 | 4 | from PySide6 import QtCore, QtWidgets 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | class SquareFrame(QtWidgets.QFrame): 9 | """Wrapper to QFrame which enforces a widget to remain square""" 10 | DESCRIPTION = "Simple wrapper to QFrame which enforces a widget to remain square" 11 | def __init__(self, parent=None): 12 | super().__init__(parent) 13 | 14 | def resizeEvent(self, event): 15 | """On resize, force the widget to remain square""" 16 | new_size = QtCore.QSize(10, 10) 17 | new_size.scale(event.size(), QtCore.Qt.AspectRatioMode.KeepAspectRatio) 18 | self.resize(new_size) 19 | 20 | def run_example_app(): 21 | """Creates a qt-app instance and runs the example, displaying a couple of square widgets""" 22 | #pylint: disable=import-outside-toplevel 23 | import sys 24 | app = QtWidgets.QApplication(sys.argv) 25 | 26 | example_window = QtWidgets.QMainWindow() 27 | central_widget = QtWidgets.QWidget() 28 | example_window.setCentralWidget(central_widget) 29 | layout = QtWidgets.QHBoxLayout() 30 | central_widget.setLayout(layout) 31 | 32 | for cur_int in range(4): 33 | widget = SquareFrame() 34 | new_btn = QtWidgets.QPushButton(f"Button {cur_int+1}") 35 | new_btn.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 36 | widget.setLayout(QtWidgets.QVBoxLayout()) 37 | widget.layout().addWidget(new_btn) 38 | layout.addWidget(widget) 39 | new_btn.show() 40 | # widget.show() 41 | example_window.show() 42 | sys.exit(app.exec()) 43 | 44 | 45 | if __name__ == "__main__": 46 | formatter = logging.Formatter("[{pathname:>90s}:{lineno:<4}] {levelname:<7s} {message}", style='{') 47 | handler = logging.StreamHandler() 48 | handler.setFormatter(formatter) 49 | logging.basicConfig( 50 | handlers=[handler], 51 | level=logging.DEBUG) #Without time 52 | #Run example 53 | run_example_app() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | #Generated using pipreqs 2 | numpy==1.23.5 3 | pandas==1.5.2 4 | PySide6==6.5.1.1 5 | setuptools==65.5.0 6 | winshell==0.6; platform_system=="Windows" 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = "pyside6_utils", 5 | version= "1.2.2", 6 | packages=find_packages('.'), 7 | description="A collection of useful widgets and utilities for PySide6 compatible with pyside6-designer.", 8 | long_description=open('README.md').read(), 9 | long_description_content_type="text/markdown", #Long description is in markdown 10 | author="Wouter Stokman", 11 | url="https://github.com/Woutah/pyside6-utils", 12 | license="LPGPLv2", 13 | include_package_data=True, 14 | install_requires=[ 15 | 'pandas>=1.5.2', #Works for 1.23.5 16 | 'numpy>=1.0.0', #Works for 1.23.5 17 | 'PySide6>=6.0.0', # Qt for Python, works for 6.5.1.1 18 | 'pathos>=0.3.0', #Works for 0.3.0 19 | 'setuptools>=65.0.0', #Works for 65.5.0 20 | 'winshell>=0.6; platform_system == "Windows"' 21 | ] 22 | ) --------------------------------------------------------------------------------