├── src └── harvesters_gui │ ├── _private │ ├── __init__.py │ └── frontend │ │ ├── __init__.py │ │ ├── image │ │ ├── __init__.py │ │ ├── icon │ │ │ ├── __init__.py │ │ │ ├── about.png │ │ │ ├── pause.png │ │ │ ├── add_file.png │ │ │ ├── collapse.png │ │ │ ├── connect.png │ │ │ ├── expand.png │ │ │ ├── update.png │ │ │ ├── disconnect.png │ │ │ ├── edit_list.png │ │ │ ├── open_file.png │ │ │ ├── genicam_logo.png │ │ │ ├── select_file.png │ │ │ ├── genicam_logo_i.kra │ │ │ ├── genicam_logo_i.png │ │ │ ├── device_attribute.png │ │ │ ├── start_acquisition.png │ │ │ └── stop_acquisition.png │ │ └── background │ │ │ ├── __init__.py │ │ │ └── about.jpg │ │ ├── pyqt5 │ │ ├── __init__.py │ │ ├── icon.py │ │ ├── helper.py │ │ ├── display_rate_list.py │ │ ├── action.py │ │ ├── device_list.py │ │ ├── thread.py │ │ ├── about.py │ │ ├── attribute_controller.py │ │ └── feature_tree.py │ │ ├── helper.py │ │ └── canvas.py │ ├── frontend │ ├── __init__.py │ └── pyqt5.py │ ├── __init__.py │ ├── _helper.py │ ├── logging │ ├── logging.report.ini │ └── logging.ini │ └── _version.py ├── docs ├── reference │ ├── index.rst │ └── harvester_core.rst ├── index.rst └── conf.py ├── MANIFEST.in ├── setup.cfg ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── CONTRIBUTING.md ├── launcher.py ├── distribute.sh ├── setup.py ├── LICENSE.txt └── README.rst /src/harvesters_gui/_private/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/harvesters_gui/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/background/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | API Reference 3 | ************* 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | harvester_core 9 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/about.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/pause.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/add_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/add_file.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/collapse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/collapse.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/connect.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/expand.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/update.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/disconnect.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/edit_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/edit_list.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/open_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/open_file.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/background/about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/background/about.jpg -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/genicam_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/genicam_logo.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/select_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/select_file.png -------------------------------------------------------------------------------- /src/harvesters_gui/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from ._version import get_versions 3 | __version__ = get_versions()['version'] 4 | if not __version__: 5 | __version__ = '1.0.2' 6 | del get_versions 7 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/genicam_logo_i.kra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/genicam_logo_i.kra -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/genicam_logo_i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/genicam_logo_i.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/device_attribute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/device_attribute.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/start_acquisition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/start_acquisition.png -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/image/icon/stop_acquisition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genicam/harvesters_gui/HEAD/src/harvesters_gui/_private/frontend/image/icon/stop_acquisition.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Versioneer 2 | include versioneer.py 3 | 4 | # 5 | global-exclude __pycache__ 6 | global-exclude *.py[co] 7 | global-exclude \.DS_Store 8 | 9 | # Misc 10 | include LICENSE.txt 11 | include README.rst 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440-pre 4 | versionfile_source = _version.py 5 | versionfile_build = build/_version.py 6 | tag_prefix = 7 | parentdir_prefix = harvesters 8 | 9 | [metadata] 10 | license_file = LICENSE.txt 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | For developers, 2 | 3 | Thank you for taking your time for Harvester. Your contribution is always welcome but excepting fixing typos or obvious defects, we would strongly encourage you to create a development branch for your trial to prevent making the trunk corrupt. 4 | 5 | We are not willing to enforce you to follow any restrict rule but it might be a good idea to introduce you a naming convention for creating a development branch: a development branch shall have a prefix ``_dev`` with your name and terminate it with a simple and obvious title. So it would look like ``_dev_kznr_issue_123`` or ``_dev_kznr_extending_foo``; the former one is preferred because we can quickly jump to the issue page just taking a look at the issue number. 6 | 7 | Once you completed the work on your branch, please give us a pull request. 8 | 9 | Thank you again for your contribution. We proud of you as a person who creates something meaningful for the human beings. 10 | 11 | Best regards, 12 | Kazunari 13 | -------------------------------------------------------------------------------- /launcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2021 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | import sys 22 | from PyQt5.QtWidgets import QApplication 23 | from harvesters_gui.frontend.pyqt5 import Harvester 24 | 25 | 26 | if __name__ == "__main__": 27 | app = QApplication(sys.argv) 28 | h = Harvester() 29 | h.show() 30 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /src/harvesters_gui/_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | import os 23 | 24 | # Related third party imports 25 | 26 | # Local application/library specific imports 27 | 28 | 29 | def get_package_root(): 30 | return os.path.dirname(os.path.abspath(__file__)) 31 | 32 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | 25 | # Local application/library specific imports 26 | 27 | 28 | def compose_tooltip(description, shortcut_key=None): 29 | tooltip = description 30 | if shortcut_key is not None: 31 | tooltip += ' (' + shortcut_key + ')' 32 | return tooltip 33 | 34 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/icon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | from PyQt5.QtGui import QIcon 25 | 26 | # Local application/library specific imports 27 | from harvesters_gui._helper import get_package_root 28 | 29 | 30 | class Icon(QIcon): 31 | def __init__(self, file_name): 32 | # 33 | super().__init__( 34 | get_package_root() + '/_private/frontend/image/icon/' + file_name 35 | ) 36 | -------------------------------------------------------------------------------- /docs/reference/harvester_core.rst: -------------------------------------------------------------------------------- 1 | ************** 2 | Harvester Core 3 | ************** 4 | 5 | Classes 6 | ======= 7 | 8 | .. autoclass:: harvesters.core.Buffer 9 | :members: 10 | :show-inheritance: 11 | :inherited-members: 12 | 13 | .. autoclass:: harvesters.core.Component1D 14 | :members: 15 | :show-inheritance: 16 | :inherited-members: 17 | 18 | .. autoclass:: harvesters.core.Component2D 19 | :members: 20 | :show-inheritance: 21 | :inherited-members: 22 | 23 | .. autoclass:: harvesters.core.ComponentBase 24 | :members: 25 | :show-inheritance: 26 | :inherited-members: 27 | 28 | .. autoclass:: harvesters.core.Harvester 29 | :members: 30 | :show-inheritance: 31 | :inherited-members: 32 | 33 | .. autoclass:: harvesters.core.ImageAcquisitionManager 34 | :members: 35 | :show-inheritance: 36 | :inherited-members: 37 | 38 | .. autoclass:: harvesters.core.PayloadBase 39 | :members: 40 | :show-inheritance: 41 | :inherited-members: 42 | 43 | .. autoclass:: harvesters.core.PayloadImage 44 | :members: 45 | :show-inheritance: 46 | :inherited-members: 47 | 48 | .. autoclass:: harvesters.core.PayloadMultiPart 49 | :members: 50 | :show-inheritance: 51 | :inherited-members: 52 | 53 | .. autoclass:: harvesters.core.ThreadBase 54 | :members: 55 | :show-inheritance: 56 | :inherited-members: 57 | 58 | Exceptions 59 | ========== 60 | 61 | 62 | Enumerations 63 | ============ 64 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | from PyQt5.QtGui import QFont 25 | 26 | # Local application/library specific imports 27 | from harvesters._private.core.helper.system import is_running_on_macos, \ 28 | is_running_on_windows 29 | 30 | 31 | def get_system_font(): 32 | if is_running_on_windows(): 33 | font, size = 'Calibri', 12 34 | else: 35 | if is_running_on_macos(): 36 | font, size = 'Lucida Sans Unicode', 14 37 | else: 38 | font, size = 'Sans-serif', 11 39 | return QFont(font, size) 40 | -------------------------------------------------------------------------------- /src/harvesters_gui/logging/logging.report.ini: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the 'License'); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an 'AS IS' BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | [loggers] 21 | keys=root,harvesters 22 | 23 | [handlers] 24 | keys=stream_handler,file_handler 25 | 26 | [formatters] 27 | keys=formatter 28 | 29 | [logger_root] 30 | level=DEBUG 31 | handlers=stream_handler 32 | 33 | [logger_harvesters] 34 | level=DEBUG 35 | handlers=stream_handler,file_handler 36 | qualname=harvesters 37 | propagate=0 38 | 39 | [handler_stream_handler] 40 | class=StreamHandler 41 | level=DEBUG 42 | formatter=formatter 43 | args=(sys.stderr,) 44 | 45 | [handler_file_handler] 46 | class=FileHandler 47 | level=DEBUG 48 | formatter=formatter 49 | args=('harvesters.log', 'a', 'utf-8', 'False') 50 | 51 | [formatter_formatter] 52 | format=%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s 53 | -------------------------------------------------------------------------------- /src/harvesters_gui/logging/logging.ini: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the 'License'); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an 'AS IS' BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | [loggers] 21 | keys=root,harvesters 22 | 23 | [handlers] 24 | keys=stream_handler 25 | #keys=stream_handler,file_handler 26 | 27 | [formatters] 28 | keys=formatter 29 | 30 | [logger_root] 31 | level=DEBUG 32 | handlers=stream_handler 33 | 34 | [logger_harvesters] 35 | level=DEBUG 36 | handlers=stream_handler 37 | #handlers=stream_handler,file_handler 38 | qualname=harvesters 39 | propagate=0 40 | 41 | [handler_stream_handler] 42 | class=StreamHandler 43 | level=DEBUG 44 | formatter=formatter 45 | args=(sys.stderr,) 46 | 47 | [handler_file_handler] 48 | class=FileHandler 49 | level=DEBUG 50 | formatter=formatter 51 | args=('harvesters.log', 'a', 'utf-8', 'False') 52 | 53 | [formatter_formatter] 54 | format=%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s 55 | -------------------------------------------------------------------------------- /distribute.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the 'License'); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an 'AS IS' BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | # Prepare options. 21 | build= 22 | test= 23 | upload= 24 | 25 | while getopts btu opt ; do 26 | case $opt in 27 | b) 28 | build=true ;; 29 | t) 30 | test=true ;; 31 | u) 32 | upload=true ;; 33 | \?) 34 | echo "Invalid option!" 35 | exit 1 ;; 36 | esac 37 | done 38 | 39 | # Delete intermediate directories. 40 | for dir in build dist genicam.harvester.egg-info 41 | do 42 | if [ -e "$dir" ] 43 | then 44 | echo "Removing \"$dir\"" 45 | rm -rf "$dir" 46 | fi 47 | done 48 | 49 | # BUild a distribution package. 50 | if [ "x$build" = "xtrue" ] 51 | then 52 | python3 setup.py sdist bdist_wheel 53 | fi 54 | 55 | url="https://upload.pypi.org/legacy/" 56 | if [ "x$test" = "xtrue" ] 57 | then 58 | url="https://test.pypi.org/legacy/" 59 | fi 60 | 61 | # Upload the distribution package. 62 | if [ "x$upload" = "xtrue" ] 63 | then 64 | twine upload --repository-url $url dist/* 65 | fi 66 | 67 | exit $? 68 | 69 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/display_rate_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | from PyQt5.QtWidgets import QComboBox 25 | 26 | # Local application/library specific imports 27 | from harvesters_gui._private.frontend.pyqt5.helper import get_system_font 28 | 29 | 30 | class ComboBoxDisplayRateList(QComboBox): 31 | # 32 | _dict_disp_rates = {'30 fps': 0, '60 fps': 1} 33 | 34 | def __init__(self, parent=None): 35 | super().__init__(parent) 36 | self.setFont(get_system_font()) 37 | for d in self._dict_disp_rates: 38 | self.addItem(d) 39 | self.setCurrentIndex(self._dict_disp_rates['30 fps']) 40 | self.currentTextChanged.connect(self._set_display_rate) 41 | 42 | def _set_display_rate(self, value): 43 | if value == '30 fps': 44 | display_rate = 30. 45 | else: 46 | display_rate = 60. 47 | self.parent().parent().canvas.display_rate = display_rate 48 | 49 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/action.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | from PyQt5.QtWidgets import QAction 25 | 26 | # Local application/library specific imports 27 | from harvesters._private.core.subject import Subject 28 | from harvesters_gui._private.frontend.pyqt5.icon import Icon 29 | 30 | 31 | class Action(QAction, Subject): 32 | def __init__( 33 | self, icon=None, title=None, parent=None, checkable=False, 34 | action=None, is_enabled=None 35 | ): 36 | # 37 | super().__init__(Icon(icon), title, parent) 38 | 39 | # 40 | self._dialog = None 41 | self._observers = [] 42 | self._action = action 43 | self._is_enabled = is_enabled 44 | 45 | # 46 | self.setCheckable(checkable) 47 | 48 | def execute(self): 49 | # Execute everything it's responsible for. 50 | if self._action: 51 | self._action() 52 | 53 | # Update itself. 54 | self.update() 55 | 56 | # Update its observers. 57 | self.update_observers() 58 | 59 | def update(self): 60 | if self._is_enabled: 61 | self.setEnabled(self._is_enabled()) 62 | self._update() 63 | 64 | def _update(self): 65 | pass 66 | 67 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/device_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | from PyQt5.QtWidgets import QComboBox 25 | 26 | # Local application/library specific imports 27 | from harvesters._private.core.observer import Observer 28 | from harvesters_gui._private.frontend.pyqt5.helper import get_system_font 29 | 30 | 31 | class ComboBoxDeviceList(QComboBox, Observer): 32 | def __init__(self, parent=None): 33 | super().__init__(parent) 34 | self.setFont(get_system_font()) 35 | 36 | def update(self): 37 | if self.parent().parent().harvester_core.has_revised_device_info_list: 38 | self.clear() 39 | separator = '::' 40 | for d in self.parent().parent().harvester_core.device_info_list: 41 | name = d.vendor 42 | name += separator 43 | name += d.model 44 | 45 | try: 46 | _ = d.serial_number 47 | except: # We know it's too broad: 48 | pass 49 | else: 50 | if d.serial_number != '': 51 | name += separator 52 | name += d.serial_number 53 | 54 | try: 55 | _ = d.user_defined_name 56 | except: # We know it's too broad: 57 | pass 58 | else: 59 | if d.user_defined_name != '': 60 | name += separator 61 | name += d.user_defined_name 62 | 63 | self.addItem(name) 64 | # 65 | self.parent().parent().harvester_core.has_revised_device_info_list = False 66 | 67 | # 68 | enable = False 69 | if self.parent().parent().cti_files: 70 | if self.parent().parent().ia is None: 71 | enable = True 72 | self.setEnabled(enable) 73 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ######################### 2 | Harvester's Documentation 3 | ######################### 4 | 5 | *Even though we just wanted to research image processing algorithms, why did we have to change our image acquisition library every time we change the camera that we use for the research? 6 | - Anonymous* 7 | 8 | *************** 9 | About Harvester 10 | *************** 11 | 12 | Harvester was created to be a public and friendly image acquisition library for all people who those want to learn computer/machine vision. Technically speaking, Harvester is a Python library which is responsible for the following tasks: 13 | 14 | * Image acquisition 15 | * Device manipulation 16 | * Image data visualization (optional) 17 | 18 | Harvester consumes image acquisition libraries, so-called GenTL Producers. If you have an officially certified GenTL Producer and GenICam compliant machine vision cameras, then Harvester supply you the acquired image data as `numpy `_ array to make your image processing task productive. 19 | 20 | You can freely use, modify, distribute Harvester under `Apache License-2.0 `_ without worrying about the use of your software: personal, internal or commercial. 21 | 22 | Currently, Harvester is being developed and maintained by the motivated volunteer contributors from all over the world. 23 | 24 | *************************** 25 | Why is it called Harvester? 26 | *************************** 27 | 28 | Harvester's name was derived from the great Flemish painter, Pieter Bruegel the Elder's painting so-called "The Harvesters". Harvesters harvest a crop every season that has been fully grown and the harvested crop is passed to the consumers. On the other hand, image acquisition libraries acquire images as their crop and the images are passed to the following processes. We found the similarity between them and decided to name our library Harvester. 29 | 30 | Apart from anything else, we love its peaceful and friendly name. We hope you also like it ;-) 31 | 32 | .. figure:: https://user-images.githubusercontent.com/8652625/40595190-1e16e90e-626e-11e8-9dc7-207d691c6d6d.jpg 33 | :align: center 34 | :alt: The Harvesters 35 | :scale: 55 % 36 | 37 | Pieter Bruegel the Elder, The Harvesters, 1565, (c) 2000–2018 The Metropolitan Museum of Art 38 | 39 | **************** 40 | Asking questions 41 | **************** 42 | 43 | We have prepared a chat room in Gitter. Please don't hesitate to drop your message when you get a question regarding Harvester! 44 | 45 | https://gitter.im/genicam-harvester/chatroom 46 | 47 | ************** 48 | External links 49 | ************** 50 | 51 | * `GitHub `_: This is the main repository of Harvester 52 | * `PyPI `_: This is the package distribution page of Harvester which is located in PyPI 53 | * `Read the Docs `_: You can find the API reference of Harvester Core and Harvester GUI 54 | 55 | ***************** 56 | Table of Contents 57 | ***************** 58 | 59 | .. toctree:: 60 | :maxdepth: 2 61 | 62 | reference/index 63 | 64 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | from PyQt5.QtCore import QMutexLocker, QThread 25 | 26 | # Local application/library specific imports 27 | from harvesters.core import ThreadBase 28 | 29 | 30 | class _PyQtThread(ThreadBase): 31 | def __init__(self, parent=None, mutex=None, worker=None, update_cycle_us=1): 32 | # 33 | super().__init__(mutex=mutex) 34 | 35 | # 36 | self._thread = _ThreadImpl( 37 | parent=parent, base=self, worker=worker, 38 | update_cycle_us=update_cycle_us 39 | ) 40 | 41 | def acquire(self): 42 | return self._thread.acquire() 43 | 44 | def release(self): 45 | self._thread.release() 46 | 47 | @property 48 | def worker(self): 49 | return self._thread.worker 50 | 51 | @worker.setter 52 | def worker(self, obj): 53 | self._thread.worker = obj 54 | 55 | @property 56 | def mutex(self): 57 | return self._mutex 58 | 59 | @property 60 | def id_(self): 61 | return self._thread.id_ 62 | 63 | def is_running(self) -> bool: 64 | return self._is_running 65 | 66 | def join(self): 67 | pass 68 | 69 | def _internal_stop(self): 70 | self._thread.stop() 71 | self._thread.wait() 72 | self._is_running = False 73 | 74 | def _internal_start(self) -> None: 75 | self._is_running = True 76 | self._thread.start() 77 | 78 | 79 | class _ThreadImpl(QThread): 80 | def __init__(self, parent=None, base=None, worker=None, 81 | update_cycle_us=1): 82 | # 83 | super().__init__(parent) 84 | 85 | # 86 | self._worker = worker 87 | self._base = base 88 | self._update_cycle_us = update_cycle_us 89 | 90 | def stop(self): 91 | with QMutexLocker(self._base.mutex): 92 | self._base._is_running = False 93 | 94 | def run(self): 95 | while self._base.is_running(): 96 | if self._worker: 97 | self._worker() 98 | # Force the current thread to sleep for some microseconds: 99 | self.usleep(self._update_cycle_us) 100 | 101 | def acquire(self): 102 | return QMutexLocker(self._base.mutex) 103 | 104 | def release(self): 105 | pass 106 | 107 | @property 108 | def worker(self): 109 | return self._worker 110 | 111 | @worker.setter 112 | def worker(self, obj): 113 | self._worker = obj 114 | 115 | @property 116 | def id_(self): 117 | return int(self.currentThreadId()) 118 | 119 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the 'License'); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an 'AS IS' BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | import os 23 | import setuptools 24 | from distutils import log 25 | import sys 26 | 27 | # Related third party imports 28 | 29 | # Local application/library specific imports 30 | import versioneer as versioneer 31 | 32 | # 33 | log.set_verbosity(log.DEBUG) 34 | log.info('Entered setup.py') 35 | log.info('$PATH=%s' % os.environ['PATH']) 36 | 37 | 38 | # Check the Python version: 39 | supported_versions = [(3, 5), (3, 6), (3, 7), (3, 8)] 40 | if sys.version_info in supported_versions: 41 | raise RuntimeError( 42 | 'See https://github.com/genicam/harvesters_gui#requirements' 43 | ) 44 | 45 | 46 | with open('README.rst', 'r',encoding='utf-8_sig') as fh: 47 | __doc__ = fh.read() 48 | 49 | description = 'Graphical user interfce of Harvester' 50 | 51 | # Determine the base directory: 52 | base_dir = os.path.dirname(__file__) 53 | src_dir = os.path.join(base_dir, 'src') 54 | 55 | # Make our package importable when executing setup.py; 56 | # the package is located in src_dir: 57 | sys.path.insert(0, src_dir) 58 | 59 | setuptools.setup( 60 | # The author of the package: 61 | author='The GenICam Committee', 62 | author_email='genicam@list.stemmer-imaging.com', 63 | # Tells the index and pip some additional metadata about our package: 64 | classifiers=( 65 | 'Development Status :: 3 - Alpha', 66 | 'Intended Audience :: Science/Research', 67 | 'Intended Audience :: Education', 68 | 'Intended Audience :: Developers', 69 | 'License :: OSI Approved :: Apache Software License', 70 | 'Operating System :: MacOS :: MacOS X', 71 | 'Operating System :: Microsoft :: Windows', 72 | 'Operating System :: POSIX', 73 | 'Programming Language :: Python :: 3.5', 74 | 'Programming Language :: Python :: 3.6', 75 | 'Programming Language :: Python :: 3.7', 76 | 'Programming Language :: Python :: 3.8', 77 | ), 78 | # A short, on-sentence summary of the package: 79 | description=description, 80 | # Location where the package may be downloaded: 81 | download_url='https://pypi.org/project/harvesters_gui/', 82 | # A list of required Python modules: 83 | install_requires=[ 84 | 'PyQt5<=5.13', 85 | 'vispy<=0.6', 86 | 'harvesters>=1.1', 87 | ], 88 | 89 | license='Apache Software License V2.0', 90 | # A detailed description of the package: 91 | long_description=__doc__, 92 | # The index to tell what type of markup is used for the long description: 93 | long_description_content_type='text/x-rst', 94 | # The name of the package: 95 | name='harvesters_gui', 96 | # A list of all Python import packages that should be included in the 97 | # distribution package: 98 | packages=setuptools.find_packages(where='src'), 99 | # Keys: Package names; an empty name stands for the root package. 100 | # Values: Directory names relative to the setup.py. 101 | package_dir={ 102 | '': 'src' 103 | }, 104 | # Keys: Package names. 105 | # Values: A list of globs. 106 | # All the files that match package_data will be added to the MANIFEST 107 | # file if no template is provided: 108 | package_data={ 109 | 'harvesters_gui': [ 110 | os.path.join( 111 | '_private', 'frontend', 'image', '*', '*.jpg' 112 | ), 113 | os.path.join( 114 | '_private', 'frontend', 'image', '*', '*.png' 115 | ), 116 | os.path.join( 117 | 'logging', '*.ini' 118 | ), 119 | ] 120 | }, 121 | # A list of supported platforms: 122 | platforms='any', 123 | # 124 | provides=['harvesters_gui'], 125 | # The URL for the website of the project: 126 | url='https://github.com/genicam/harvesters_gui', 127 | # The package version: 128 | version=versioneer.get_version(), 129 | ) 130 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/about.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | import sys 23 | 24 | # Related third party imports 25 | from PyQt5.QtCore import Qt 26 | from PyQt5.QtGui import QPainter, QPixmap 27 | from PyQt5.QtWidgets import QDialog, QApplication, QPlainTextEdit, \ 28 | QVBoxLayout, QHBoxLayout, QLineEdit, QFrame, QPushButton, \ 29 | QTextEdit 30 | 31 | # Local application/library specific imports 32 | from harvesters.__init__ import __version__ 33 | from harvesters_gui._helper import get_package_root 34 | from harvesters_gui._private.frontend.pyqt5.helper import get_system_font 35 | 36 | 37 | class DecoratedDialog(QDialog): 38 | def __init__(self, parent=None, path_to_image=None): 39 | # 40 | super().__init__(parent) 41 | 42 | # 43 | self._path_to_image = path_to_image 44 | 45 | def paintEvent(self, event): 46 | painter = QPainter(self) 47 | painter.drawPixmap( 48 | self.rect(), 49 | QPixmap(get_package_root() + '/_private/frontend/image/background/about.jpg') 50 | ) 51 | 52 | 53 | class TransparentLineEdit(QLineEdit): 54 | def __init__(self, text): 55 | # 56 | super().__init__(text) 57 | 58 | self.setReadOnly(True) 59 | self.setFont(get_system_font()) 60 | self.setStyleSheet('background: rgb(0, 0, 0, 0%)') 61 | self.setFrame(False) 62 | 63 | 64 | class TransparentTextEdit(QTextEdit): 65 | def __init__(self, text): 66 | # 67 | super().__init__(text) 68 | 69 | self.setReadOnly(True) 70 | self.setFont(get_system_font()) 71 | self.setStyleSheet('background: rgb(0, 0, 0, 0%)') 72 | self.setLineWrapMode(True) 73 | self.setFrameStyle(QFrame.NoFrame) 74 | self.setAlignment(Qt.AlignCenter) 75 | 76 | 77 | class About(QDialog): 78 | def __init__(self, parent=None): 79 | # 80 | super().__init__(parent) 81 | 82 | # 83 | self.setWindowTitle('About Harvester') 84 | 85 | # 86 | layout_main = QVBoxLayout() 87 | layout_textual_info = QVBoxLayout() 88 | layout_image = QHBoxLayout() 89 | 90 | # 91 | self._button_acknowledgements = QPushButton() 92 | self._button_acknowledgements.setText('Acknowledgements') 93 | self._button_acknowledgements.setFont(get_system_font()) 94 | self._button_acknowledgements.clicked.connect( 95 | self._handle_open_dialog 96 | ) 97 | 98 | # 99 | text_version = TransparentLineEdit( 100 | 'Version: ' + __version__ 101 | ) 102 | text_copyright = TransparentLineEdit('Copyright (c) 2018 EMVA') 103 | 104 | layout_textual_info.addWidget(text_version) 105 | layout_textual_info.addWidget(text_copyright) 106 | 107 | # 108 | image = DecoratedDialog() 109 | image.setFixedWidth(640) 110 | image.setFixedHeight(480) 111 | 112 | # 113 | layout_image.addWidget(image) 114 | 115 | # 116 | layout_main.addLayout(layout_image) 117 | layout_main.addLayout(layout_textual_info) 118 | layout_main.addWidget(self._button_acknowledgements) 119 | 120 | # 121 | self.setLayout(layout_main) 122 | 123 | # 124 | self._acknowledgements = Acknowledgements(self) 125 | self._acknowledgements.setModal(True) 126 | 127 | def _get_version_info(self): 128 | return 'Version ' + self.parent().version 129 | 130 | def _handle_open_dialog(self): 131 | self._acknowledgements.show() 132 | 133 | 134 | class Acknowledgements(QDialog): 135 | def __init__(self, parent=None): 136 | # 137 | super().__init__(parent=parent) 138 | 139 | # 140 | self.setWindowTitle('Acknowledgements') 141 | 142 | # 143 | layout = QVBoxLayout(self) 144 | 145 | # 146 | content = 'Cover Drawing:\n' 147 | content += '\n' 148 | content += 'Pieter Bruegel the Elder, The Harvesters\n' 149 | content += 'Copyright (c) 2000–2018 The Metropolitan Museum of Arts' 150 | content += '\n\n' 151 | content += 'Open Source Libraries/Resources:\n' 152 | content += '\n' 153 | content += 'VisPy (BSD)\n' 154 | content += 'Copyright (c) 2013-2018 VisPy developers\n' 155 | content += 'http://vispy.org/' 156 | content += '\n\n' 157 | content += 'PyQt5 (GPL)\n' 158 | content += 'Copyright (c) 2018 Riverbank Computing Limited\n' 159 | content += 'https://www.riverbankcomputing.com/' 160 | content += '\n\n' 161 | content += 'Icons8 (Creative Commons Attribution-NoDerivs 3.0 Unported)\n' 162 | content += 'Copyright (c) Icons8 LLC\n' 163 | content += 'https://icons8.comn' 164 | content += '\n\n' 165 | content += 'Versioneer (Public Domain, CC0-1.0)\n' 166 | content += 'Copyright (c) 2018 Brian Warner\n' 167 | content += 'https://github.com/warner/python-versioneer' 168 | 169 | self._text = QPlainTextEdit(content) 170 | self._text.setReadOnly(True) 171 | self._text.setFont(get_system_font()) 172 | self._text.setLineWrapMode(True) 173 | self._text.setFixedWidth(480) 174 | 175 | layout.addWidget(self._text) 176 | self.setLayout(layout) 177 | 178 | 179 | if __name__ == '__main__': 180 | app = QApplication(sys.argv) 181 | about = About() 182 | about.show() 183 | sys.exit(app.exec_()) 184 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/attribute_controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | import sys 23 | 24 | # Related third party imports 25 | from PyQt5.QtCore import pyqtSlot 26 | from PyQt5.QtGui import QKeySequence 27 | from PyQt5.QtWidgets import QMainWindow, QApplication, QTreeView, \ 28 | QAction, QComboBox, QLineEdit, QLabel, QShortcut 29 | 30 | from genicam.genapi import EVisibility 31 | 32 | # Local application/library specific imports 33 | from harvesters_gui._private.frontend.helper import compose_tooltip 34 | from harvesters_gui._private.frontend.pyqt5.action import Action 35 | from harvesters_gui._private.frontend.pyqt5.feature_tree import \ 36 | FeatureEditDelegate, FilterProxyModel, FeatureTreeModel 37 | from harvesters_gui._private.frontend.pyqt5.helper import get_system_font 38 | 39 | """ 40 | 41 | If you got into a trouble relate to model, the following tool could give 42 | you a hint. ModelTest is a Python script and it tests your model and report 43 | the result: 44 | 45 | https://github.com/bgr/PyQt5_modeltest 46 | 47 | """ 48 | 49 | 50 | class ActionExpandAll(Action): 51 | def __init__(self, icon=None, title=None, parent=None, action=None): 52 | # 53 | super().__init__( 54 | icon=icon, title=title, parent=parent, action=action 55 | ) 56 | 57 | 58 | class ActionCollapseAll(Action): 59 | def __init__(self, icon=None, title=None, parent=None, action=None): 60 | # 61 | super().__init__( 62 | icon=icon, title=title, parent=parent, action=action 63 | ) 64 | 65 | 66 | class AttributeController(QMainWindow): 67 | _visibility_dict = { 68 | 'Beginner': EVisibility.Beginner, 69 | 'Expert': EVisibility.Expert, 70 | 'Guru': EVisibility.Guru, 71 | 'All': EVisibility.Invisible, 72 | } 73 | 74 | def __init__(self, node_map, parent=None): 75 | # 76 | super().__init__(parent=parent) 77 | 78 | # 79 | self.setWindowTitle('Attribute Controller') 80 | 81 | # 82 | self._view = QTreeView() 83 | self._view.setFont(get_system_font()) 84 | 85 | # 86 | self._node_map = node_map 87 | self._model = FeatureTreeModel( 88 | node_map=self._node_map, 89 | ) 90 | 91 | # 92 | self._proxy = FilterProxyModel() 93 | self._proxy.setSourceModel(self._model) 94 | self._proxy.setDynamicSortFilter(False) 95 | 96 | # 97 | self._delegate = FeatureEditDelegate(proxy=self._proxy) 98 | self._view.setModel(self._proxy) 99 | self._view.setItemDelegate(self._delegate) 100 | self._view.setUniformRowHeights(True) 101 | 102 | # 103 | unit = 260 104 | for i in range(2): 105 | self._view.setColumnWidth(i, unit) 106 | 107 | w, h = 700, 600 108 | self._view.setGeometry(100, 100, w, h) 109 | 110 | self.setCentralWidget(self._view) 111 | self.setGeometry(100, 100, unit * 2, 640) 112 | 113 | self._combo_box_visibility = None 114 | self._line_edit_search_box = None 115 | 116 | # 117 | self._setup_toolbars() 118 | 119 | def _setup_toolbars(self): 120 | # 121 | group_filter = self.addToolBar('Node Visibility') 122 | group_manipulation = self.addToolBar('Node Tree Manipulation') 123 | 124 | # 125 | label_visibility = QLabel() 126 | label_visibility.setText('Visibility') 127 | label_visibility.setFont(get_system_font()) 128 | 129 | # 130 | self._combo_box_visibility = QComboBox() 131 | self._combo_box_visibility.setSizeAdjustPolicy( 132 | QComboBox.AdjustToContents 133 | ) 134 | 135 | # 136 | items = ('Beginner', 'Expert', 'Guru', 'All') 137 | for item in items: 138 | self._combo_box_visibility.addItem(item) 139 | 140 | shortcut_key = 'Ctrl+v' 141 | shortcut = QShortcut(QKeySequence(shortcut_key), self) 142 | 143 | def show_popup(): 144 | self._combo_box_visibility.showPopup() 145 | 146 | shortcut.activated.connect(show_popup) 147 | 148 | self._combo_box_visibility.setToolTip( 149 | compose_tooltip('Filter the nodes to show', shortcut_key) 150 | ) 151 | self._combo_box_visibility.setFont(get_system_font()) 152 | self._combo_box_visibility.currentIndexChanged.connect( 153 | self._invalidate_feature_tree_by_visibility 154 | ) 155 | 156 | # 157 | button_expand_all = ActionExpandAll( 158 | icon='expand.png', title='Expand All', parent=self, 159 | action=self.action_on_expand_all 160 | ) 161 | shortcut_key = 'Ctrl+e' 162 | button_expand_all.setToolTip( 163 | compose_tooltip('Expand the node tree', shortcut_key) 164 | ) 165 | button_expand_all.setShortcut(shortcut_key) 166 | button_expand_all.toggle() 167 | 168 | # 169 | button_collapse_all = ActionCollapseAll( 170 | icon='collapse.png', title='Collapse All', parent=self, 171 | action=self.action_on_collapse_all 172 | ) 173 | shortcut_key = 'Ctrl+c' 174 | button_collapse_all.setToolTip( 175 | compose_tooltip('Collapse the node tree', shortcut_key) 176 | ) 177 | button_collapse_all.setShortcut(shortcut_key) 178 | button_collapse_all.toggle() 179 | 180 | # 181 | label_search = QLabel() 182 | label_search.setText('RegEx Search') 183 | label_search.setFont(get_system_font()) 184 | 185 | # 186 | self._line_edit_search_box = QLineEdit() 187 | self._line_edit_search_box.setFont(get_system_font()) 188 | self._line_edit_search_box.textEdited.connect( 189 | self._invalidate_feature_tree_by_keyword 190 | ) 191 | 192 | # 193 | group_filter.addWidget(label_visibility) 194 | group_filter.addWidget(self._combo_box_visibility) 195 | group_filter.addWidget(label_search) 196 | group_filter.addWidget(self._line_edit_search_box) 197 | group_filter.setStyleSheet('QToolBar{spacing:6px;}') 198 | 199 | # 200 | group_manipulation.addAction(button_expand_all) 201 | group_manipulation.addAction(button_collapse_all) 202 | 203 | # 204 | group_manipulation.actionTriggered[QAction].connect( 205 | self.on_button_clicked_action 206 | ) 207 | 208 | # 209 | self._combo_box_visibility.setCurrentIndex( 210 | self._visibility_dict['Expert'] 211 | ) 212 | 213 | def _invalidate_feature_tree_by_visibility(self): 214 | visibility = self._visibility_dict[ 215 | self._combo_box_visibility.currentText() 216 | ] 217 | self._proxy.setVisibility(visibility) 218 | self._view.expandAll() 219 | 220 | @pyqtSlot('QString') 221 | def _invalidate_feature_tree_by_keyword(self, keyword): 222 | self._proxy.setKeyword(keyword) 223 | self._view.expandAll() 224 | 225 | @staticmethod 226 | def on_button_clicked_action(action): 227 | action.execute() 228 | 229 | def expand_all(self): 230 | self._view.expandAll() 231 | 232 | def collapse_all(self): 233 | self._view.collapseAll() 234 | 235 | def resize_column_width(self): 236 | for i in range(self._model.columnCount()): 237 | self._view.resizeColumnToContents(i) 238 | 239 | def action_on_expand_all(self): 240 | self.expand_all() 241 | 242 | def action_on_collapse_all(self): 243 | self.collapse_all() 244 | 245 | 246 | if __name__ == '__main__': 247 | app = QApplication(sys.argv) 248 | about = AttributeController() 249 | about.show() 250 | sys.exit(app.exec_()) 251 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 EMVA 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # GenTL-Python Binding documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Jun 22 06:21:22 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('../harvesters/')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.intersphinx', 35 | 'sphinx.ext.ifconfig', 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.autosummary', 38 | 'sphinx.ext.napoleon', 39 | 'sphinx.ext.todo' 40 | ] 41 | 42 | # TO-DO 43 | todo_include_todos = True 44 | 45 | # Napoleon settings 46 | napoleon_google_docstring = True 47 | napoleon_numpy_docstring = True 48 | napoleon_include_init_with_doc = False 49 | napoleon_include_private_with_doc = False 50 | napoleon_include_special_with_doc = True 51 | napoleon_use_admonition_for_examples = False 52 | napoleon_use_admonition_for_notes = False 53 | napoleon_use_admonition_for_references = False 54 | napoleon_use_ivar = False 55 | napoleon_use_param = True 56 | napoleon_use_rtype = True 57 | 58 | # Add any paths that contain templates here, relative to this directory. 59 | templates_path = ['_templates'] 60 | 61 | # The suffix(es) of source filenames. 62 | # You can specify multiple suffix as a list of string: 63 | # 64 | source_suffix = '.rst' 65 | 66 | # The encoding of source files. 67 | # 68 | # source_encoding = 'utf-8-sig' 69 | 70 | # The master toctree document. 71 | master_doc = 'index' 72 | 73 | # General information about the project. 74 | project = 'Harvester' 75 | copyright = '2018 EMVA' 76 | author = 'EMVA' 77 | 78 | # The version info for the project you're documenting, acts as replacement for 79 | # |version| and |release|, also used in various other places throughout the 80 | # built documents. 81 | # 82 | # The short X.Y version. 83 | version = '0.0.0' 84 | # The full version, including alpha/beta/rc tags. 85 | release = '0' 86 | 87 | # The language for content autogenerated by Sphinx. Refer to documentation 88 | # for a list of supported languages. 89 | # 90 | # This is also used if you do content translation via gettext catalogs. 91 | # Usually you set "language" from the command line for these cases. 92 | language = None 93 | 94 | # There are two options for replacing |today|: either, you set today to some 95 | # non-false value, then it is used: 96 | # 97 | # today = '' 98 | # 99 | # Else, today_fmt is used as the format for a strftime call. 100 | # 101 | # today_fmt = '%B %d, %Y' 102 | 103 | # List of patterns, relative to source directory, that match files and 104 | # directories to ignore when looking for source files. 105 | # This patterns also effect to html_static_path and html_extra_path 106 | exclude_patterns = [] 107 | 108 | # The reST default role (used for this markup: `text`) to use for all 109 | # documents. 110 | # 111 | # default_role = None 112 | 113 | # If true, '()' will be appended to :func: etc. cross-reference text. 114 | # 115 | # add_function_parentheses = True 116 | 117 | # If true, the current module name will be prepended to all description 118 | # unit titles (such as .. function::). 119 | # 120 | # add_module_names = True 121 | 122 | # If true, sectionauthor and moduleauthor directives will be shown in the 123 | # output. They are ignored by default. 124 | # 125 | # show_authors = False 126 | 127 | # The name of the Pygments (syntax highlighting) style to use. 128 | pygments_style = 'sphinx' 129 | 130 | # A list of ignored prefixes for module index sorting. 131 | # modindex_common_prefix = [] 132 | 133 | # If true, keep warnings as "system message" paragraphs in the built documents. 134 | # keep_warnings = False 135 | 136 | # If true, `todo` and `todoList` produce output, else they produce nothing. 137 | todo_include_todos = False 138 | 139 | 140 | # -- Options for HTML output ---------------------------------------------- 141 | 142 | # The theme to use for HTML and HTML Help pages. See the documentation for 143 | # a list of builtin themes. 144 | # 145 | #html_theme = 'bizstyle' 146 | 147 | 148 | # Theme options are theme-specific and customize the look and feel of a theme 149 | # further. For a list of options available for each theme, see the 150 | # documentation. 151 | # 152 | # html_theme_options = {} 153 | 154 | # Add any paths that contain custom themes here, relative to this directory. 155 | # html_theme_path = [] 156 | 157 | # The name for this set of Sphinx documents. 158 | # " v documentation" by default. 159 | # 160 | # html_title = 'GenTL-Python Binding v0.0.1' 161 | 162 | # A shorter title for the navigation bar. Default is the same as html_title. 163 | # 164 | # html_short_title = None 165 | 166 | # The name of an image file (relative to this directory) to place at the top 167 | # of the sidebar. 168 | # 169 | # html_logo = None 170 | #html_logo = '../../_images/genicam_logo.png' 171 | 172 | # The name of an image file (relative to this directory) to use as a favicon of 173 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 174 | # pixels large. 175 | # 176 | # html_favicon = None 177 | 178 | # Add any paths that contain custom static files (such as style sheets) here, 179 | # relative to this directory. They are copied after the builtin static files, 180 | # so a file named "default.css" will overwrite the builtin "default.css". 181 | # html_static_path = ['_static'] 182 | 183 | # Add any extra paths that contain custom files (such as robots.txt or 184 | # .htaccess) here, relative to this directory. These files are copied 185 | # directly to the root of the documentation. 186 | # 187 | # html_extra_path = [] 188 | 189 | # If not None, a 'Last updated on:' timestamp is inserted at every page 190 | # bottom, using the given strftime format. 191 | # The empty string is equivalent to '%b %d, %Y'. 192 | # 193 | # html_last_updated_fmt = None 194 | 195 | # If true, SmartyPants will be used to convert quotes and dashes to 196 | # typographically correct entities. 197 | # 198 | # html_use_smartypants = True 199 | 200 | # Custom sidebar templates, maps document names to template names. 201 | # 202 | # html_sidebars = {} 203 | 204 | # Additional templates that should be rendered to pages, maps page names to 205 | # template names. 206 | # 207 | # html_additional_pages = {} 208 | 209 | # If false, no module index is generated. 210 | # 211 | # html_domain_indices = True 212 | 213 | # If false, no index is generated. 214 | # 215 | # html_use_index = True 216 | 217 | # If true, the index is split into individual pages for each letter. 218 | # 219 | # html_split_index = False 220 | 221 | # If true, links to the reST sources are added to the pages. 222 | # 223 | # html_show_sourcelink = True 224 | 225 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 226 | # 227 | # html_show_sphinx = True 228 | 229 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 230 | # 231 | # html_show_copyright = True 232 | 233 | # If true, an OpenSearch description file will be output, and all pages will 234 | # contain a tag referring to it. The value of this option must be the 235 | # base URL from which the finished HTML is served. 236 | # 237 | # html_use_opensearch = '' 238 | 239 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 240 | # html_file_suffix = None 241 | 242 | # Language to be used for generating the HTML full-text search index. 243 | # Sphinx supports the following languages: 244 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 245 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 246 | # 247 | # html_search_language = 'en' 248 | 249 | # A dictionary with options for the search language support, empty by default. 250 | # 'ja' uses this config value. 251 | # 'zh' user can custom change `jieba` dictionary path. 252 | # 253 | # html_search_options = {'type': 'default'} 254 | 255 | # The name of a javascript file (relative to the configuration directory) that 256 | # implements a search results scorer. If empty, the default will be used. 257 | # 258 | # html_search_scorer = 'scorer.js' 259 | 260 | # Output file base name for HTML help builder. 261 | htmlhelp_basename = 'HarvesterDoc' 262 | 263 | # -- Options for LaTeX output --------------------------------------------- 264 | 265 | latex_elements = { 266 | # The paper size ('letterpaper' or 'a4paper'). 267 | # 268 | # 'papersize': 'letterpaper', 269 | 270 | # The font size ('10pt', '11pt' or '12pt'). 271 | # 272 | # 'pointsize': '10pt', 273 | 274 | # Additional stuff for the LaTeX preamble. 275 | # 276 | # 'preamble': '', 277 | 278 | # Latex figure (float) alignment 279 | # 280 | # 'figure_align': 'htbp', 281 | } 282 | 283 | # Grouping the document tree into LaTeX files. List of tuples 284 | # (source start file, target name, title, 285 | # author, documentclass [howto, manual, or own class]). 286 | latex_documents = [ 287 | (master_doc, 'Harvester.tex', 'Harvester Documentation', 288 | 'EMVA', 'manual'), 289 | ] 290 | 291 | # The name of an image file (relative to this directory) to place at the top of 292 | # the title page. 293 | # 294 | # latex_logo = None 295 | 296 | # For "manual" documents, if this is true, then toplevel headings are parts, 297 | # not chapters. 298 | # 299 | # latex_use_parts = False 300 | 301 | # If true, show page references after internal links. 302 | # 303 | # latex_show_pagerefs = False 304 | 305 | # If true, show URL addresses after external links. 306 | # 307 | # latex_show_urls = False 308 | 309 | # Documents to append as an appendix to all manuals. 310 | # 311 | # latex_appendices = [] 312 | 313 | # It false, will not define \strong, \code, itleref, \crossref ... but only 314 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 315 | # packages. 316 | # 317 | # latex_keep_old_macro_names = True 318 | 319 | # If false, no module index is generated. 320 | # 321 | # latex_domain_indices = True 322 | 323 | 324 | # -- Options for manual page output --------------------------------------- 325 | 326 | # One entry per manual page. List of tuples 327 | # (source start file, name, description, authors, manual section). 328 | man_pages = [ 329 | (master_doc, 'harvesters', 'Harvester Documentation', 330 | [author], 1) 331 | ] 332 | 333 | # If true, show URL addresses after external links. 334 | # 335 | # man_show_urls = False 336 | 337 | 338 | # -- Options for Texinfo output ------------------------------------------- 339 | 340 | # Grouping the document tree into Texinfo files. List of tuples 341 | # (source start file, target name, title, author, 342 | # dir menu entry, description, category) 343 | texinfo_documents = [ 344 | (master_doc, 'Harvester', 'Harvester Documentation', 345 | author, 'Harvester', 'One line description of project.', 346 | 'Miscellaneous'), 347 | ] 348 | 349 | # Documents to append as an appendix to all manuals. 350 | # 351 | # texinfo_appendices = [] 352 | 353 | # If false, no module index is generated. 354 | # 355 | # texinfo_domain_indices = True 356 | 357 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 358 | # 359 | # texinfo_show_urls = 'footnote' 360 | 361 | # If true, do not generate a @detailmenu in the "Top" node's menu. 362 | # 363 | # texinfo_no_detailmenu = False 364 | 365 | epub_author = 'EMVA' 366 | epub_publisher = 'EMVA' 367 | 368 | # Example configuration for intersphinx: refer to the Python standard library. 369 | intersphinx_mapping = { 370 | 'https://docs.python.org/' + \ 371 | str(sys.version_info[0]) + '.' + \ 372 | str(sys.version_info[1]): None 373 | } 374 | 375 | # List up the module to be mocked. 376 | autodoc_mock_imports = ['genicam'] 377 | 378 | def skip(app, what, name, obj, skip, options): 379 | if name == "__init__": 380 | return False 381 | return skip 382 | 383 | def setup(app): 384 | app.connect("autodoc-skip-member", skip) 385 | 386 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/canvas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | 23 | # Related third party imports 24 | 25 | import numpy as np 26 | 27 | from vispy import gloo 28 | from vispy import app 29 | from vispy.gloo import Program 30 | from vispy.util.transforms import ortho 31 | 32 | from genicam.gentl import PAYLOADTYPE_INFO_IDS 33 | from genicam.gentl import TimeoutException 34 | 35 | # Local application/library specific imports 36 | from harvesters._private.core.helper.system import is_running_on_macos 37 | from harvesters.util.pfnc import is_custom, get_bits_per_pixel, \ 38 | bgr_formats 39 | from harvesters.util.pfnc import mono_location_formats, \ 40 | rgb_formats, bgr_formats, \ 41 | rgba_formats, bgra_formats, \ 42 | bayer_location_formats 43 | 44 | 45 | class CanvasBase(app.Canvas): 46 | def __init__( 47 | self, *, 48 | image_acquirer=None, 49 | width=640, height=480, 50 | display_rate=30., 51 | background_color='gray', 52 | vsync=True 53 | ): 54 | """ 55 | As far as we know, Vispy refreshes the canvas every 1/30 sec at the 56 | fastest no matter which faster number is specified. If we set any 57 | value which is greater than 30, then Vispy's callback is randomly 58 | called. 59 | """ 60 | 61 | # 62 | app.Canvas.__init__( 63 | self, size=(width, height), vsync=vsync, autoswap=True 64 | ) 65 | 66 | # 67 | self._ia = image_acquirer 68 | 69 | # 70 | self._background_color = background_color 71 | self._has_filled_texture = False 72 | self._width, self._height = width, height 73 | 74 | # 75 | self._is_dragging = False 76 | 77 | # If it's True, the canvas keeps image acquisition but do not 78 | # draw images on the canvas: 79 | self._pause_drawing = False 80 | 81 | # 82 | self._origin = [0, 0] 83 | 84 | # 85 | self._display_rate = display_rate 86 | self._timer = app.Timer( 87 | 1. / self._display_rate, connect=self.update, start=True 88 | ) 89 | 90 | # 91 | self._buffers = [] 92 | 93 | @property 94 | def display_rate(self): 95 | return self._display_rate 96 | 97 | @display_rate.setter 98 | def display_rate(self, value): 99 | self._display_rate = value 100 | self._timer.stop() 101 | self._timer.start(interval=1./self._display_rate) 102 | 103 | def set_canvas_size(self, width, height): 104 | # 105 | self._has_filled_texture = False 106 | 107 | # 108 | updated = False 109 | 110 | # 111 | if self._width != width or self._height != height: 112 | self._width = width 113 | self._height = height 114 | updated = True 115 | 116 | # 117 | if updated: 118 | self.apply_magnification() 119 | 120 | def on_draw(self, event): 121 | # Update on June 15th, 2018: 122 | # According to a VisPy developer, they have not finished 123 | # porting VisPy to PyQt5. Once they finished the development 124 | # we should try it out if it gives us the maximum refresh rate. 125 | # See the following URL to check the latest information: 126 | # 127 | # https://github.com/vispy/vispy/issues/1394 128 | 129 | # Clear the canvas in gray. 130 | gloo.clear(color=self._background_color) 131 | 132 | drew = False 133 | try: 134 | if not self._pause_drawing: 135 | # Fetch a buffer. 136 | buffer = self.ia.fetch(timeout=0.0001) 137 | 138 | # Prepare a texture to draw: 139 | self._prepare_texture(buffer) 140 | 141 | # Draw the texture until the buffer object exists 142 | # within this scope: 143 | # (We keep the buffer until the next one is delivered to 144 | # keep the current chunk data alive but it depends on the 145 | # application; we just want to tell you that the texture 146 | # must be overdrawn until the content is alive:) 147 | self._draw() 148 | 149 | # Release the buffers that we've kept holding so far: 150 | self.release_buffers() 151 | 152 | # We have drawn the latest image on the canvas: 153 | drew = True 154 | 155 | # Keep the buffer alive to keep the chunk data alive until 156 | # the next one is delivered: 157 | self._buffers.append(buffer) 158 | 159 | except AttributeError: 160 | # Calling fetch_buffer() raises AttributeError because 161 | # the ImageAcquirer object is None. 162 | pass 163 | except TimeoutException: 164 | # We have an ImageAcquirer object but nothing has 165 | # been fetched, wait for the next round: 166 | pass 167 | 168 | # Draw the latest texture again if needed: 169 | if not drew: 170 | self._draw() 171 | 172 | def release_buffers(self): 173 | for _buffer in self._buffers: 174 | if _buffer: 175 | _buffer.queue() 176 | self._buffers.clear() 177 | 178 | def _draw(self): 179 | raise NotImplementedError 180 | 181 | def on_resize(self, event): 182 | self.apply_magnification() 183 | 184 | def apply_magnification(self): 185 | raise NotImplementedError 186 | 187 | def on_mouse_wheel(self, event): 188 | raise NotImplementedError 189 | 190 | def on_mouse_press(self, event): 191 | self._is_dragging = True 192 | self._origin = event.pos 193 | 194 | def on_mouse_release(self, event): 195 | self._is_dragging = False 196 | 197 | def on_mouse_move(self, event): 198 | raise NotImplementedError 199 | 200 | def pause_drawing(self, pause=True): 201 | self._pause_drawing = pause 202 | 203 | def toggle_drawing(self): 204 | self._pause_drawing = False if self._pause_drawing else True 205 | 206 | def is_pausing(self): 207 | return True if self._pause_drawing else False 208 | 209 | def resume_drawing(self): 210 | self._pause_drawing = False 211 | 212 | @property 213 | def background_color(self): 214 | return self._background_color 215 | 216 | @background_color.setter 217 | def background_color(self, color): 218 | self._background_color = color 219 | 220 | @property 221 | def ia(self): 222 | return self._ia 223 | 224 | @ia.setter 225 | def ia(self, value): 226 | self._ia = value 227 | 228 | def _prepare_texture(self, buffer): 229 | raise NotImplementedError 230 | 231 | 232 | class Canvas2D(CanvasBase): 233 | _visible_payloads = [ 234 | PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_IMAGE, 235 | PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_CHUNK_DATA, 236 | PAYLOADTYPE_INFO_IDS.PAYLOAD_TYPE_MULTI_PART, 237 | ] 238 | 239 | def __init__( 240 | self, *, 241 | image_acquirer=None, 242 | width=640, height=480, 243 | background_color='gray', 244 | vsync=True, display_rate=30. 245 | ): 246 | # 247 | super().__init__( 248 | image_acquirer=image_acquirer, 249 | width=width, height=height, 250 | display_rate=display_rate, 251 | background_color=background_color, 252 | vsync=vsync 253 | ) 254 | 255 | # 256 | self._vertex_shader = """ 257 | // Uniforms 258 | uniform mat4 u_model; 259 | uniform mat4 u_view; 260 | uniform mat4 u_projection; 261 | 262 | // Attributes 263 | attribute vec2 a_position; 264 | attribute vec2 a_texcoord; 265 | 266 | // Varyings 267 | varying vec2 v_texcoord; 268 | 269 | // Main 270 | void main (void) 271 | { 272 | v_texcoord = a_texcoord; 273 | gl_Position = u_projection * u_view * u_model * vec4(a_position, 0.0, 1.0); 274 | } 275 | """ 276 | 277 | self._fragment_shader = """ 278 | varying vec2 v_texcoord; 279 | uniform sampler2D texture; 280 | void main() 281 | { 282 | gl_FragColor = texture2D(texture, v_texcoord); 283 | } 284 | """ 285 | 286 | # 287 | self._program = None 288 | self._data = None 289 | self._coordinate = None 290 | self._translate = 0. 291 | self._latest_translate = self._translate 292 | self._magnification = 1. 293 | 294 | # Apply shaders. 295 | self._program = Program( 296 | self._vertex_shader, self._fragment_shader, count=4 297 | ) 298 | 299 | # 300 | self._data = np.zeros( 301 | 4, dtype=[ 302 | ('a_position', np.float32, 2), 303 | ('a_texcoord', np.float32, 2) 304 | ] 305 | ) 306 | 307 | # 308 | self._data['a_texcoord'] = np.array( 309 | [[0., 1.], [1., 1.], [0., 0.], [1., 0.]] 310 | ) 311 | 312 | # 313 | self._program['u_model'] = np.eye(4, dtype=np.float32) 314 | self._program['u_view'] = np.eye(4, dtype=np.float32) 315 | 316 | # 317 | self._coordinate = [0, 0] 318 | 319 | 320 | # 321 | self._program['texture'] = np.zeros( 322 | (self._height, self._width), dtype='uint8' 323 | ) 324 | 325 | # 326 | self.apply_magnification() 327 | 328 | def _prepare_texture(self, buffer): 329 | update = True 330 | if buffer.payload_type not in self._visible_payloads: 331 | update = False 332 | 333 | # Set the image as the texture of our canvas. 334 | if buffer: 335 | # 336 | payload = buffer.payload 337 | component = payload.components[0] 338 | width = component.width 339 | height = component.height 340 | 341 | # Update the canvas size if needed. 342 | self.set_canvas_size(width, height) 343 | 344 | # 345 | exponent = 0 346 | data_format = None 347 | 348 | # 349 | data_format_value = component.data_format_value 350 | if is_custom(data_format_value): 351 | update = False 352 | else: 353 | data_format = component.data_format 354 | bpp = get_bits_per_pixel(data_format) 355 | if bpp is not None: 356 | exponent = bpp - 8 357 | else: 358 | update = False 359 | 360 | if update: 361 | # Reshape the image so that it can be drawn on the 362 | # VisPy canvas: 363 | if data_format in mono_location_formats or \ 364 | data_format in bayer_location_formats: 365 | # Reshape the 1D NumPy array into a 2D so that VisPy 366 | # can display it as a mono image: 367 | content = component.data.reshape(height, width) 368 | else: 369 | # The image requires you to reshape it to draw it on the 370 | # canvas: 371 | if data_format in rgb_formats or \ 372 | data_format in rgba_formats or \ 373 | data_format in bgr_formats or \ 374 | data_format in bgra_formats: 375 | # Reshape the 1D NumPy array into a 2D so that VisPy 376 | # can display it as an RGB image: 377 | content = component.data.reshape( 378 | height, width, 379 | int(component.num_components_per_pixel) 380 | ) 381 | # 382 | if data_format in bgr_formats: 383 | # Swap every R and B so that VisPy can display 384 | # it as an RGB image: 385 | content = content[:, :, ::-1] 386 | else: 387 | return 388 | 389 | # Convert each data to an 8bit. 390 | if exponent > 0: 391 | # The following code may affect to the rendering 392 | # performance: 393 | content = (content / (2 ** exponent)) 394 | 395 | # Then cast each array element to an uint8: 396 | content = content.astype(np.uint8) 397 | 398 | self._program['texture'] = content 399 | 400 | def _draw(self): 401 | self._program.draw('triangle_strip') 402 | 403 | def apply_magnification(self): 404 | # 405 | canvas_w, canvas_h = self.physical_size 406 | gloo.set_viewport(0, 0, canvas_w, canvas_h) 407 | 408 | # 409 | ratio = self._magnification 410 | w, h = self._width, self._height 411 | 412 | self._program['u_projection'] = ortho( 413 | self._coordinate[0], 414 | canvas_w * ratio + self._coordinate[0], 415 | self._coordinate[1], 416 | canvas_h * ratio + self._coordinate[1], 417 | -1, 1 418 | ) 419 | 420 | x, y = int((canvas_w * ratio - w) / 2), int((canvas_h * ratio - h) / 2) # centering x & y 421 | 422 | # 423 | self._data['a_position'] = np.array( 424 | [[x, y], [x + w, y], [x, y + h], [x + w, y + h]] 425 | ) 426 | 427 | # 428 | self._program.bind(gloo.VertexBuffer(self._data)) 429 | 430 | def on_mouse_wheel(self, event): 431 | self._translate += event.delta[1] 432 | power = 7. if is_running_on_macos() else 5. # 2 ** exponent 433 | stride = 4. if is_running_on_macos() else 7. 434 | translate = self._translate 435 | translate = min(power * stride, translate) 436 | translate = max(-power * stride, translate) 437 | self._translate = translate 438 | self._magnification = 2 ** -(self._translate / stride) 439 | if self._latest_translate != self._translate: 440 | self.apply_magnification() 441 | self._latest_translate = self._translate 442 | 443 | def on_mouse_move(self, event): 444 | if self._is_dragging: 445 | adjustment = 2. if is_running_on_macos() else 1. 446 | ratio = self._magnification * adjustment 447 | delta = event.pos - self._origin 448 | self._origin = event.pos 449 | self._coordinate[0] -= (delta[0] * ratio) 450 | self._coordinate[1] += (delta[1] * ratio) 451 | self.apply_magnification() 452 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. figure:: https://user-images.githubusercontent.com/8652625/40595190-1e16e90e-626e-11e8-9dc7-207d691c6d6d.jpg 2 | :align: center 3 | :alt: The Harvesters 4 | 5 | Pieter Bruegel the Elder, The Harvesters, 1565, (c) The Metropolitan Museum of Art 6 | 7 | .. image:: https://img.shields.io/pypi/v/harvesters_gui.svg 8 | :target: https://pypi.org/project/harvesters_gui 9 | 10 | .. image:: https://img.shields.io/pypi/pyversions/harvesters_gui.svg 11 | 12 | ---- 13 | 14 | ###################### 15 | What Is Harvester GUI? 16 | ###################### 17 | 18 | Harvester GUI is a reference implementation of a `Harvester `_-based GUI. We need to admit that it must not be the one that satisfies your realuse cases but it should give you an idea how you can design a GUI that is based on Harvester as its image acquisition front-end. 19 | 20 | You can freely use, modify, distribute Harvester under `Apache License-2.0 `_ without worrying about the use of your software: personal, internal or commercial. 21 | 22 | 23 | ---- 24 | 25 | .. contents:: Table of Contents 26 | :depth: 1 27 | 28 | **Disclaimer**: All external pictures should have associated credits. If there are missing credits, please tell us, we will correct it. Similarly, all excerpts should be sourced. If not, this is an error and we will correct it as soon as you tell us. 29 | 30 | ---- 31 | 32 | ############# 33 | Announcements 34 | ############# 35 | 36 | - **Version 1.0.2**: Resolves issue `#22 `_. 37 | - **Version 1.0.1**: Resolves issue `#17 `_. 38 | - **Version 1.0.0**: Resolves issue `#16 `_. 39 | - **Version 0.4.0**: Resolves issue `#15 `_. 40 | - **Version 0.3.0**: Resolves issues `#11 `_ and `#14 `_. 41 | - **Version 0.2.6**: Resolves issue `#13 `_. 42 | - **Version 0.2.5**: Use Harvester version ``0.2.4``. 43 | - **Version 0.2.4**: Use Harvester version ``0.2.3``. 44 | - **Version 0.2.3**: Use Harvester version ``0.2.2``. 45 | - **Version 0.2.2**: Resolves issue `#5 `_. 46 | - **Version 0.2.1**: Works with Harvester versions >= ``0.2.1``. 47 | - **Version 0.2.0**: Works with Harvester versions >= ``0.2.0``. 48 | - **Version 0.1.0**: Note that this version will be deprecated and the following versions will be incompatible with any of ``0.1.n`` versions. 49 | 50 | ************** 51 | External links 52 | ************** 53 | 54 | * `Harvester `_: Image acquisition front-end library 55 | * `PyPI `_: This is the package distribution page of Harvester which is located in PyPI 56 | * `Read the Docs `_: You can find the API reference of Harvester and Harvester GUI 57 | 58 | ############ 59 | Installation 60 | ############ 61 | 62 | In this section, we will learn how to instruct procedures to get Harvester work. 63 | 64 | ******************* 65 | System Requirements 66 | ******************* 67 | 68 | The following software modules are required to get Harvester working: 69 | 70 | * Python>=3.5,<3.9 71 | 72 | In addition, you will need the following items to let Harvester make something meaningful: 73 | 74 | * GenTL Producers 75 | * GenICam compliant machine vision cameras 76 | 77 | ************************ 78 | Installing Harvester GUI 79 | ************************ 80 | 81 | If you want to use Harvester GUI, then please invoke the following command: 82 | 83 | .. code-block:: shell 84 | 85 | $ pip install harvesters_gui 86 | 87 | Note that ``PyQt5`` is distributed under LGPL and it may not be ideal for your purpose. In the future, we might support other GUI frameworks which are more or less open and free. 88 | 89 | *********************** 90 | Launching Harvester GUI 91 | *********************** 92 | 93 | To launch Harvester GUI, let's create a Python script file, naming ``harvester.py``, that contains the following code: 94 | 95 | .. code-block:: python 96 | 97 | import sys 98 | from PyQt5.QtWidgets import QApplication 99 | from harvesters_gui.frontend.pyqt5 import Harvester 100 | 101 | if __name__ == '__main__': 102 | app = QApplication(sys.argv) 103 | h = Harvester() 104 | h.show() 105 | sys.exit(app.exec_()) 106 | 107 | Then launch ``harvester.py``: 108 | 109 | .. code-block:: shell 110 | 111 | $ python path/to/harvester.py 112 | 113 | You will see Harvester GUI pops up. 114 | 115 | ########################### 116 | How does Harvester GUI us? 117 | ########################### 118 | 119 | Harvester GUI works on the top of Harvester and offers you high-performance image data visualization on the fly. It involves VisPy for controlling OpenGL functionality and PyQt for providing GUI. 120 | 121 | The main features of Harvester GUI are listed as follows: 122 | 123 | * Image data visualization of the acquired images 124 | * Image magnification using a mouse wheel or a trackpad 125 | * Image dragging using a mouse or a trackpad 126 | * An arbitrary selection of image displaying point in the data path (Not implemented yet) 127 | 128 | Unlike Harvester, Harvester GUI limits the number of GenTL Producers to load just one. This is just a limitation to not make the GUI complicated. In general, the user should know which GenTL Producer should be loaded to control his target remote device. It's not necessary to load multiple GenTL Producers for this use case. However, this is just an idea in an early stage. We might support multiple loading on even Harvester GUI in the future. 129 | 130 | ########### 131 | Screenshots 132 | ########### 133 | 134 | In this section, we see some useful windows which Harvester GUI offers you. 135 | 136 | **************************** 137 | Image data visualizer window 138 | **************************** 139 | 140 | The image data visualizer window (below) offers you a visualization of the acquired images. In this screenshot, Harvester is acquiring a 4000 x 3000 pixel of RGB8 image at 30 fps; it means it's acquiring images at 8.6 Gbps. It's quite fast, isn't it? 141 | 142 | .. image:: https://user-images.githubusercontent.com/8652625/43035346-c84fe404-8d28-11e8-815f-2df66cbbc6d0.png 143 | :align: center 144 | :alt: Image data visualizer 145 | 146 | *************************** 147 | Attribute controller window 148 | *************************** 149 | 150 | The attribute controller window (below) offers you to manipulate GenICam feature nodes of the target remote device. Changing exposure time, triggering the target remote device for image acquisition, storing a set of camera configuration so-called User Set, etc, you can manually control the target remote device anytime when you want to. It supports the visibility filter feature and regular expression feature. These features are useful in a case where you need to display only the features you are interested in. 151 | 152 | .. image:: https://user-images.githubusercontent.com/8652625/43035351-d35a2936-8d28-11e8-83d5-7b6efa6e2ad8.png 153 | :align: center 154 | :alt: Attribute Controller 155 | 156 | ################### 157 | Using Harvester GUI 158 | ################### 159 | 160 | **************************** 161 | Image data visualizer window 162 | **************************** 163 | 164 | Image data visualizer window :: Toolbar 165 | ======================================= 166 | 167 | Most of Harvester GUI's features can be used through its toolbox. In this section, we describe each button's functionality and how to use it. Regarding shortcut keys, replace ``Ctrl`` with ``Command`` on macOS. 168 | 169 | .. image:: https://user-images.githubusercontent.com/8652625/43035384-7d1109e0-8d29-11e8-9005-38b965a9680e.png 170 | :align: center 171 | :alt: Toolbar 172 | 173 | Selecting a CTI file 174 | -------------------- 175 | 176 | .. image:: https://user-images.githubusercontent.com/8652625/40596073-7e1b6a82-6273-11e8-9045-68bbbd034281.png 177 | :align: left 178 | :alt: Open file 179 | 180 | This button is used to select a GenTL Producer file to load. The shortcut key is ``Ctrl+o``. 181 | 182 | Updating the remote device information list 183 | ------------------------------------------- 184 | 185 | .. image:: https://user-images.githubusercontent.com/8652625/40596091-9354283a-6273-11e8-8c6f-559db511339a.png 186 | :align: left 187 | :alt: Update 188 | 189 | This button is used to update the remote device information list; the list will be filled up with the remote devices that are handled by the GenTL Producer that you have loaded on Harvester GUI; sometime it might be empy if there's no remote device is available. The shortcut key is ``Ctrl+u``. It might be useful when you newly connect a remote device to your system. 190 | 191 | Selecting a GenICam compliant remote device 192 | ------------------------------------------- 193 | 194 | This combo box shows a list of available GenICam compliant remote devices. You can select a remote device that you want to control. The shortcut key is ``Ctrl+D``, i.e., ``Ctrl+Shift+d``. 195 | 196 | Connecting a selected remote device to Harvester 197 | ------------------------------------------------ 198 | 199 | .. image:: https://user-images.githubusercontent.com/8652625/40596045-49c61d54-6273-11e8-8424-d16e923b5b3f.png 200 | :align: left 201 | :alt: Connect 202 | 203 | This button is used to connect a remote device which is being selected by the former combo box. The shortcut key is ``Ctrl+c``. Once you connect the remote device, the remote device is exclusively controlled. 204 | 205 | Disconnecting the connecting remote device from Harvester 206 | --------------------------------------------------------- 207 | 208 | .. image:: https://user-images.githubusercontent.com/8652625/40596046-49f0fd9e-6273-11e8-83e3-7ba8aad3c4f7.png 209 | :align: left 210 | :alt: Disconnect 211 | 212 | This button is used to disconnect the connecting remote device from Harvester. The shortcut key is ``Ctrl+d``. 213 | 214 | Starting image acquisition 215 | -------------------------- 216 | 217 | .. image:: https://user-images.githubusercontent.com/8652625/40596022-34d3d486-6273-11e8-92c3-2349be5fd98f.png 218 | :align: left 219 | :alt: Start image acquisition 220 | 221 | This button is used to start image acquisition. The shortcut key is ``Ctrl+j``. The acquired images will be drawing in the following canvas pane. 222 | 223 | Pausing/Resuming image drawing 224 | ------------------------------ 225 | 226 | .. image:: https://user-images.githubusercontent.com/8652625/40596063-6cae1aba-6273-11e8-9049-2430a042c671.png 227 | :align: left 228 | :alt: Pause 229 | 230 | This button is used to pausing/resuming drawing images on the canvas pane while it's keep acquiring images in the background. The shortcut key is ``Ctrl+k``. If you want to resume drawing images, just click the button again. You can do the same thing with the start image acquisition button (``Ctrl+j``). 231 | 232 | Stopping image acquisition 233 | -------------------------- 234 | 235 | .. image:: https://user-images.githubusercontent.com/8652625/40596024-35d84c86-6273-11e8-89b8-9368db740f22.png 236 | :align: left 237 | :alt: Stop image acquisition 238 | 239 | This button is used to stop image acquisition. The shortcut key is ``Ctrl+l``. 240 | 241 | Showing the remote device attribute dialog 242 | ------------------------------------------ 243 | 244 | .. image:: https://user-images.githubusercontent.com/8652625/40596224-7b2cf0e2-6274-11e8-9088-bb48163968d6.png 245 | :align: left 246 | :alt: Device attribute 247 | 248 | This button is used to show the remote device attribute dialog. The shortcut key is ``Ctrl+a``. The remote device attribute dialog offers you to a way to intuitively control remote device attribute over a GUI. 249 | 250 | Showing the about dialog 251 | ------------------------ 252 | 253 | .. image:: https://user-images.githubusercontent.com/8652625/40596039-449ddc36-6273-11e8-9f91-1eb7830b8e8c.png 254 | :align: left 255 | :alt: About 256 | 257 | This button is used to show the about dialog. 258 | 259 | Image data visualizer window :: Canvas 260 | ====================================== 261 | 262 | The canvas of Harvester GUI offers you not only image data visualization but also some intuitive object manipulations. 263 | 264 | .. image:: https://user-images.githubusercontent.com/8652625/43035349-cdd9f9a0-8d28-11e8-8152-0bc488450ef6.png 265 | :align: center 266 | :alt: Canvas 267 | 268 | Zooming into the displayed image 269 | -------------------------------- 270 | 271 | If you're using a mouse, spin the wheel to your pointing finger points at. If you are using a trackpad on a macOS, slide two fingers to the display side. 272 | 273 | Zooming out from the displayed image 274 | ------------------------------------ 275 | 276 | If you're using a mouse, spin the wheel to your side. If you are using a trackpad on a macOS, slide two fingers to your side. 277 | 278 | Changing the part being displayed 279 | --------------------------------- 280 | 281 | If you're using a mouse, grab any point in the canvas and drag the pointer as if you're physically grabbing the image. The image will follow the pointer. If you are using a trackpad on a macOS, it might be useful if you assign the three finger slide for dragging. 282 | 283 | *************************** 284 | Attribute controller window 285 | *************************** 286 | 287 | The attribute controller offers you an interface to each GenICam feature node that the the target remote device provides. 288 | 289 | Attribute controller window :: Toolbar 290 | ====================================== 291 | 292 | .. image:: https://user-images.githubusercontent.com/8652625/43035353-d64c96e2-8d28-11e8-8c68-0bc4ee866d28.png 293 | :align: center 294 | :alt: Toolbar 295 | 296 | Filtering GenICam feature nodes by visibility 297 | --------------------------------------------- 298 | 299 | This combo box offers you to apply visibility filter to the GenICam feature node tree. The shortcut key is ``Ctrl+v`` 300 | 301 | GenICam defines the following visibility levels: 302 | 303 | * **Beginner**: Features that should be visible for all users via the GUI and API. 304 | * **Expert**: Features that require a more in-depth knowledge of the camera functionality. 305 | * **Guru**: Advanced features that might bring the cameras into a state where it will not work properly anymore if it is set incorrectly for the cameras current mode of operation. 306 | * **Invisible**: Features that should be kept hidden for the GUI users but still be available via the API. 307 | 308 | The following table shows each item in the combo box and the visibility status of each visibility level: 309 | 310 | .. list-table:: 311 | :header-rows: 1 312 | :align: center 313 | 314 | - - Combo box item 315 | - Beginner 316 | - Expert 317 | - Guru 318 | - Invisible 319 | - - Beginner 320 | - Visible 321 | - Invisible 322 | - Invisible 323 | - Invisible 324 | - - Expert 325 | - Visible 326 | - Visible 327 | - Invisible 328 | - Invisible 329 | - - Guru 330 | - Visible 331 | - Visible 332 | - Visible 333 | - Invisible 334 | - - All 335 | - Visible 336 | - Visible 337 | - Visible 338 | - Visible 339 | 340 | Filtering GenICam feature nodes by regular expression 341 | ----------------------------------------------------- 342 | 343 | This text edit box offers you to filter GenICam feature nodes by regular expression. 344 | 345 | Expanding the feature node tree 346 | ------------------------------- 347 | 348 | .. image:: https://user-images.githubusercontent.com/8652625/41112454-f7471566-6ab9-11e8-93a4-d2d56c7bbd31.png 349 | :align: left 350 | :alt: Expand feature node tree 351 | 352 | This button is used to expand the feature node tree. The shortcut key is ``Ctrl+e``. 353 | 354 | Collapsing the feature node tree 355 | -------------------------------- 356 | 357 | .. image:: https://user-images.githubusercontent.com/8652625/41112453-f712498a-6ab9-11e8-9f9f-160c0e0d8866.png 358 | :align: left 359 | :alt: Collapse feature node tree 360 | 361 | This button is used to collapse the feature node tree. The shortcut key is ``Ctrl+c``. 362 | 363 | ################ 364 | Acknowledgements 365 | ################ 366 | 367 | ********************* 368 | Open source resources 369 | ********************* 370 | 371 | Harvester GUI uses the following open source libraries/resources: 372 | 373 | * VisPy 374 | 375 | | License: `BSD 3-Clause `_ 376 | | Copyright (c) 2013-2018 VisPy developers 377 | 378 | | http://vispy.org 379 | | https://github.com/vispy/vispy 380 | 381 | * PyQt5 382 | 383 | | License: `GPLv3 `_ 384 | | Copyright (c) 2018 Riverbank Computing Limited 385 | 386 | | https://www.riverbankcomputing.com 387 | | https://pypi.org/project/PyQt5/ 388 | 389 | * Icons8 390 | 391 | | License: `Creative Commons Attribution-NoDerivs 3.0 Unported `_ 392 | | Copyright (c) Icons8 LLC 393 | 394 | | https://icons8.com 395 | -------------------------------------------------------------------------------- /src/harvesters_gui/_private/frontend/pyqt5/feature_tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | import re 23 | import sys 24 | 25 | # Related third party imports 26 | from PyQt5.Qt import Qt, QStyledItemDelegate, QColor 27 | from PyQt5.QtCore import QAbstractItemModel, QModelIndex, \ 28 | QSortFilterProxyModel 29 | from PyQt5.QtWidgets import QApplication, QTreeView, \ 30 | QSpinBox, QPushButton, QComboBox, QWidget, \ 31 | QLineEdit 32 | 33 | from genicam.genapi import NodeMap 34 | from genicam.genapi import EInterfaceType, EAccessMode, EVisibility 35 | 36 | # Local application/library specific imports 37 | from harvesters_gui._private.frontend.pyqt5.helper import get_system_font 38 | 39 | 40 | class TreeItem(object): 41 | _readable_nodes = [ 42 | EInterfaceType.intfIBoolean, 43 | EInterfaceType.intfIEnumeration, 44 | EInterfaceType.intfIFloat, 45 | EInterfaceType.intfIInteger, 46 | EInterfaceType.intfIString, 47 | EInterfaceType.intfIRegister, 48 | ] 49 | 50 | _readable_access_modes = [EAccessMode.RW, EAccessMode.RO] 51 | 52 | def __init__(self, data=None, parent_item=None): 53 | # 54 | super().__init__() 55 | 56 | # 57 | self._parent_item = parent_item 58 | self._own_data = data 59 | self._child_items = [] 60 | 61 | @property 62 | def parent_item(self): 63 | return self._parent_item 64 | 65 | @property 66 | def own_data(self): 67 | return self._own_data 68 | 69 | @property 70 | def child_items(self): 71 | return self._child_items 72 | 73 | def appendChild(self, item): 74 | self.child_items.append(item) 75 | 76 | def child(self, row): 77 | return self.child_items[row] 78 | 79 | def childCount(self): 80 | return len(self.child_items) 81 | 82 | def columnCount(self): 83 | try: 84 | ret = len(self.own_data) 85 | except TypeError: 86 | ret = 1 87 | return ret 88 | 89 | def data(self, column): 90 | if isinstance(self.own_data[column], str): 91 | try: 92 | return self.own_data[column] 93 | except IndexError: 94 | return None 95 | else: 96 | value = '' 97 | feature = self.own_data[column] 98 | if column == 0: 99 | value = feature.node.display_name 100 | else: 101 | interface_type = feature.node.principal_interface_type 102 | 103 | if interface_type != EInterfaceType.intfICategory: 104 | if interface_type == EInterfaceType.intfICommand: 105 | value = '[Click here]' 106 | else: 107 | if feature.node.get_access_mode() not in \ 108 | self._readable_access_modes: 109 | value = '[Not accessible]' 110 | elif interface_type not in self._readable_nodes: 111 | value = '[Not readable]' 112 | else: 113 | try: 114 | value = str(feature.value) 115 | except AttributeError: 116 | try: 117 | value = feature.to_string() 118 | except AttributeError: 119 | pass 120 | 121 | return value 122 | 123 | def tooltip(self, column): 124 | if isinstance(self.own_data[column], str): 125 | return None 126 | else: 127 | feature = self.own_data[column] 128 | return feature.node.tooltip 129 | 130 | def background(self, column): 131 | if isinstance(self.own_data[column], str): 132 | return None 133 | else: 134 | feature = self.own_data[column] 135 | interface_type = feature.node.principal_interface_type 136 | if interface_type == EInterfaceType.intfICategory: 137 | return QColor(56, 147, 189) 138 | else: 139 | return None 140 | 141 | def foreground(self, column): 142 | if isinstance(self.own_data[column], str): 143 | return None 144 | else: 145 | feature = self.own_data[column] 146 | interface_type = feature.node.principal_interface_type 147 | if interface_type == EInterfaceType.intfICategory: 148 | return QColor('white') 149 | else: 150 | return None 151 | 152 | def parent(self): 153 | return self._parent_item 154 | 155 | def row(self): 156 | if self._parent_item: 157 | return self._parent_item.child_items.index(self) 158 | 159 | return 0 160 | 161 | 162 | class FeatureTreeModel(QAbstractItemModel): 163 | # 164 | _capable_roles = [ 165 | Qt.DisplayRole, Qt.ToolTipRole, Qt.BackgroundColorRole, 166 | Qt.ForegroundRole 167 | ] 168 | 169 | # 170 | _editables = [EAccessMode.RW, EAccessMode.WO] 171 | 172 | def __init__(self, parent=None, node_map: NodeMap=None): 173 | """ 174 | REMARKS: QAbstractItemModel might impact the performance and could 175 | slow Harvester. As far as we've confirmed, QAbstractItemModel calls 176 | its index() method for every item already shown. Especially, such 177 | a call happens every time when (1) its view got/lost focus or (2) 178 | its view was scrolled. If such slow performance makes people 179 | irritating we should investigate how can we optimize it. 180 | 181 | """ 182 | # 183 | super().__init__() 184 | 185 | # 186 | self._root_item = TreeItem(('Feature Name', 'Value')) 187 | self._node_map = node_map 188 | if node_map: 189 | self.populateTreeItems(node_map.Root.features, self._root_item) 190 | 191 | @property 192 | def root_item(self): 193 | return self._root_item 194 | 195 | def columnCount(self, parent=None, *args, **kwargs): 196 | if parent.isValid(): 197 | return parent.internalPointer().columnCount() 198 | else: 199 | return self.root_item.columnCount() 200 | 201 | def data(self, index: QModelIndex, role=None): 202 | if not index.isValid(): 203 | return None 204 | 205 | if role not in self._capable_roles: 206 | return None 207 | 208 | item = index.internalPointer() 209 | if role == Qt.DisplayRole: 210 | value = item.data(index.column()) 211 | elif role == Qt.ToolTipRole: 212 | value = item.tooltip(index.column()) 213 | elif role == Qt.BackgroundColorRole: 214 | value = item.background(index.column()) 215 | else: 216 | value = item.foreground(index.column()) 217 | 218 | return value 219 | 220 | def flags(self, index): 221 | if not index.isValid(): 222 | return Qt.NoItemFlags 223 | 224 | tree_item = index.internalPointer() 225 | feature = tree_item.own_data[0] 226 | access_mode = feature.node.get_access_mode() 227 | 228 | if access_mode in self._editables: 229 | ret = Qt.ItemIsEnabled | Qt.ItemIsEditable 230 | else: 231 | if index.column() == 1: 232 | ret = Qt.NoItemFlags 233 | else: 234 | ret = Qt.ItemIsEnabled 235 | return ret 236 | 237 | def headerData(self, p_int, Qt_Orientation, role=None): 238 | # p_int: section 239 | if Qt_Orientation == Qt.Horizontal and role == Qt.DisplayRole: 240 | return self.root_item.data(p_int) 241 | return None 242 | 243 | def index(self, p_int, p_int_1, parent=None, *args, **kwargs): 244 | # p_int: row 245 | # p_int_1: column 246 | if not self.hasIndex(p_int, p_int_1, parent): 247 | return QModelIndex() 248 | 249 | if not parent.isValid(): 250 | parent_item = self.root_item 251 | else: 252 | parent_item = parent.internalPointer() 253 | 254 | child_item = parent_item.child(p_int) 255 | if child_item: 256 | return self.createIndex(p_int, p_int_1, child_item) 257 | else: 258 | return QModelIndex() 259 | 260 | def parent(self, index=None): 261 | if not index.isValid(): 262 | return index 263 | 264 | child_item = index.internalPointer() 265 | parent_item = child_item.parent() 266 | 267 | if parent_item == self.root_item: 268 | return QModelIndex() 269 | 270 | return self.createIndex(parent_item.row(), 0, parent_item) 271 | 272 | def rowCount(self, parent=None, *args, **kwargs): 273 | if parent.column() > 0: 274 | return 0 275 | 276 | if not parent.isValid(): 277 | parent_item = self.root_item 278 | else: 279 | parent_item = parent.internalPointer() 280 | 281 | return parent_item.childCount() 282 | 283 | def populateTreeItems(self, features, parent_item): 284 | for feature in features: 285 | interface_type = feature.node.principal_interface_type 286 | item = TreeItem([feature, feature], parent_item) 287 | parent_item.appendChild(item) 288 | if interface_type == EInterfaceType.intfICategory: 289 | self.populateTreeItems(feature.features, item) 290 | 291 | def setData(self, index: QModelIndex, value, role=Qt.EditRole): 292 | if role == Qt.EditRole: 293 | # TODO: Check the type of the target and convert the given value. 294 | self.dataChanged.emit(index, index) 295 | 296 | # 297 | tree_item = index.internalPointer() 298 | feature = tree_item.own_data[0] 299 | interface_type = feature.node.principal_interface_type 300 | try: 301 | if interface_type == EInterfaceType.intfICommand: 302 | if value: 303 | feature.execute() 304 | elif interface_type == EInterfaceType.intfIBoolean: 305 | feature.value = True if value.lower() == 'true' else False 306 | elif interface_type == EInterfaceType.intfIFloat: 307 | feature.value = float(value) 308 | else: 309 | feature.value = value 310 | return True 311 | except: 312 | # TODO: Specify appropriate exceptions 313 | return False 314 | 315 | 316 | class FeatureEditDelegate(QStyledItemDelegate): 317 | def __init__(self, proxy, parent=None): 318 | # 319 | super().__init__() 320 | 321 | # 322 | self._proxy = proxy 323 | 324 | def createEditor(self, parent: QWidget, QStyleOptionViewItem, proxy_index: QModelIndex): 325 | 326 | # Get the actual source. 327 | src_index = self._proxy.mapToSource(proxy_index) 328 | 329 | # If it's the column #0, then immediately return. 330 | if src_index.column() == 0: 331 | return None 332 | 333 | tree_item = src_index.internalPointer() 334 | feature = tree_item.own_data[0] 335 | interface_type = feature.node.principal_interface_type 336 | 337 | if interface_type == EInterfaceType.intfIInteger: 338 | w = QSpinBox(parent) 339 | w.setRange(feature.min, feature.max) 340 | w.setSingleStep(feature.inc) 341 | w.setValue(feature.value) 342 | elif interface_type == EInterfaceType.intfICommand: 343 | w = QPushButton(parent) 344 | w.setText('Execute') 345 | w.clicked.connect(lambda: self.on_button_clicked(proxy_index)) 346 | elif interface_type == EInterfaceType.intfIBoolean: 347 | w = QComboBox(parent) 348 | boolean_ints = {'False': 0, 'True': 1} 349 | w.addItem('False') 350 | w.addItem('True') 351 | proxy_index = boolean_ints['True'] if feature.value else boolean_ints['False'] 352 | w.setCurrentIndex(proxy_index) 353 | elif interface_type == EInterfaceType.intfIEnumeration: 354 | w = QComboBox(parent) 355 | for item in feature.entries: 356 | w.addItem(item.symbolic) 357 | w.setCurrentText(feature.value) 358 | elif interface_type == EInterfaceType.intfIString: 359 | w = QLineEdit(parent) 360 | w.setText(feature.value) 361 | elif interface_type == EInterfaceType.intfIFloat: 362 | w = QLineEdit(parent) 363 | w.setText(str(feature.value)) 364 | else: 365 | return None 366 | 367 | # 368 | w.setFont(get_system_font()) 369 | 370 | return w 371 | 372 | def setEditorData(self, editor: QWidget, proxy_index: QModelIndex): 373 | 374 | src_index = self._proxy.mapToSource(proxy_index) 375 | value = src_index.data(Qt.DisplayRole) 376 | tree_item = src_index.internalPointer() 377 | feature = tree_item.own_data[0] 378 | interface_type = feature.node.principal_interface_type 379 | 380 | if interface_type == EInterfaceType.intfIInteger: 381 | editor.setValue(int(value)) 382 | elif interface_type == EInterfaceType.intfIBoolean: 383 | i = editor.findText(value, Qt.MatchFixedString) 384 | editor.setCurrentIndex(i) 385 | elif interface_type == EInterfaceType.intfIEnumeration: 386 | editor.setEditText(value) 387 | elif interface_type == EInterfaceType.intfIString: 388 | editor.setText(value) 389 | elif interface_type == EInterfaceType.intfIFloat: 390 | editor.setText(value) 391 | 392 | def setModelData(self, editor: QWidget, model: QAbstractItemModel, proxy_index: QModelIndex): 393 | 394 | src_index = self._proxy.mapToSource(proxy_index) 395 | tree_item = src_index.internalPointer() 396 | feature = tree_item.own_data[0] 397 | interface_type = feature.node.principal_interface_type 398 | 399 | if interface_type == EInterfaceType.intfIInteger: 400 | data = editor.value() 401 | model.setData(proxy_index, data) 402 | elif interface_type == EInterfaceType.intfIBoolean: 403 | data = editor.currentText() 404 | model.setData(proxy_index, data) 405 | elif interface_type == EInterfaceType.intfIEnumeration: 406 | data = editor.currentText() 407 | model.setData(proxy_index, data) 408 | elif interface_type == EInterfaceType.intfIString: 409 | data = editor.text() 410 | model.setData(proxy_index, data) 411 | elif interface_type == EInterfaceType.intfIFloat: 412 | data = editor.text() 413 | model.setData(proxy_index, data) 414 | 415 | def on_button_clicked(self, proxy_index: QModelIndex): 416 | 417 | src_index = self._proxy.mapToSource(proxy_index) 418 | tree_item = src_index.internalPointer() 419 | feature = tree_item.own_data[0] 420 | interface_type = feature.node.principal_interface_type 421 | 422 | if interface_type == EInterfaceType.intfICommand: 423 | feature.execute() 424 | 425 | 426 | class FilterProxyModel(QSortFilterProxyModel): 427 | def __init__(self, visibility=EVisibility.Beginner, parent=None): 428 | # 429 | super().__init__() 430 | 431 | # 432 | self._visibility = visibility 433 | self._keyword = '' 434 | 435 | def filterVisibility(self, visibility): 436 | beginner_items = {EVisibility.Beginner} 437 | expert_items = beginner_items.union({EVisibility.Expert}) 438 | guru_items = expert_items.union({EVisibility.Guru}) 439 | all_items = guru_items.union({EVisibility.Invisible}) 440 | 441 | items_dict = { 442 | EVisibility.Beginner: beginner_items, 443 | EVisibility.Expert: expert_items, 444 | EVisibility.Guru: guru_items, 445 | EVisibility.Invisible: all_items 446 | } 447 | 448 | if visibility not in items_dict[self._visibility]: 449 | return False 450 | else: 451 | return True 452 | 453 | def filterPattern(self, name): 454 | if not re.search(self._keyword, name, re.IGNORECASE): 455 | print(name + ': refused') 456 | return False 457 | else: 458 | print(name + ': accepted') 459 | return True 460 | 461 | def setVisibility(self, visibility: EVisibility): 462 | self._visibility = visibility 463 | self.invalidateFilter() 464 | 465 | def setKeyword(self, keyword: str): 466 | self._keyword = keyword 467 | self.invalidateFilter() 468 | 469 | def filterAcceptsRow(self, src_row, src_parent: QModelIndex): 470 | # 471 | src_model = self.sourceModel() 472 | src_index = src_model.index(src_row, 0, parent=src_parent) 473 | 474 | tree_item = src_index.internalPointer() 475 | feature = tree_item.own_data[0] 476 | name = feature.node.display_name 477 | visibility = feature.node.visibility 478 | if len(tree_item.child_items): 479 | for child in tree_item.child_items: 480 | if self.filterAcceptsRow(child.row(), src_index): 481 | return True 482 | return False 483 | else: 484 | matches = re.search(self._keyword, name, re.IGNORECASE) 485 | 486 | if matches: 487 | result = self.filterVisibility(visibility) 488 | else: 489 | result = False 490 | return result 491 | 492 | 493 | if __name__ == '__main__': 494 | app = QApplication(sys.argv) 495 | model = FeatureTreeModel() 496 | view = QTreeView(model) 497 | view.show() 498 | sys.exit(app.exec_()) 499 | -------------------------------------------------------------------------------- /src/harvesters_gui/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = "$Format:%d$" 27 | git_full = "$Format:%H$" 28 | git_date = "$Format:%ci$" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "pep440-pre" 44 | cfg.tag_prefix = "" 45 | cfg.parentdir_prefix = "harvesters" 46 | cfg.versionfile_source = "_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_old(pieces): 384 | """TAG[.postDISTANCE[.dev0]] . 385 | 386 | The ".dev0" means dirty. 387 | 388 | Eexceptions: 389 | 1: no tags. 0.postDISTANCE[.dev0] 390 | """ 391 | if pieces["closest-tag"]: 392 | rendered = pieces["closest-tag"] 393 | if pieces["distance"] or pieces["dirty"]: 394 | rendered += ".post%d" % pieces["distance"] 395 | if pieces["dirty"]: 396 | rendered += ".dev0" 397 | else: 398 | # exception #1 399 | rendered = "0.post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | return rendered 403 | 404 | 405 | def render_git_describe(pieces): 406 | """TAG[-DISTANCE-gHEX][-dirty]. 407 | 408 | Like 'git describe --tags --dirty --always'. 409 | 410 | Exceptions: 411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"]: 416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 | else: 418 | # exception #1 419 | rendered = pieces["short"] 420 | if pieces["dirty"]: 421 | rendered += "-dirty" 422 | return rendered 423 | 424 | 425 | def render_git_describe_long(pieces): 426 | """TAG-DISTANCE-gHEX[-dirty]. 427 | 428 | Like 'git describe --tags --dirty --always -long'. 429 | The distance/hash is unconditional. 430 | 431 | Exceptions: 432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 | """ 434 | if pieces["closest-tag"]: 435 | rendered = pieces["closest-tag"] 436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 | else: 438 | # exception #1 439 | rendered = pieces["short"] 440 | if pieces["dirty"]: 441 | rendered += "-dirty" 442 | return rendered 443 | 444 | 445 | def render(pieces, style): 446 | """Render the given version pieces into the requested style.""" 447 | if pieces["error"]: 448 | return {"version": "unknown", 449 | "full-revisionid": pieces.get("long"), 450 | "dirty": None, 451 | "error": pieces["error"], 452 | "date": None} 453 | 454 | if not style or style == "default": 455 | style = "pep440" # the default 456 | 457 | if style == "pep440": 458 | rendered = render_pep440(pieces) 459 | elif style == "pep440-pre": 460 | rendered = render_pep440_pre(pieces) 461 | elif style == "pep440-post": 462 | rendered = render_pep440_post(pieces) 463 | elif style == "pep440-old": 464 | rendered = render_pep440_old(pieces) 465 | elif style == "git-describe": 466 | rendered = render_git_describe(pieces) 467 | elif style == "git-describe-long": 468 | rendered = render_git_describe_long(pieces) 469 | else: 470 | raise ValueError("unknown style '%s'" % style) 471 | 472 | return {"version": rendered, "full-revisionid": pieces["long"], 473 | "dirty": pieces["dirty"], "error": None, 474 | "date": pieces.get("date")} 475 | 476 | 477 | def get_versions(): 478 | """Get version information or return default if unable to do so.""" 479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 | # __file__, we can work backwards from there to the root. Some 481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 | # case we can only use expanded keywords. 483 | 484 | cfg = get_config() 485 | verbose = cfg.verbose 486 | 487 | try: 488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 | verbose) 490 | except NotThisMethod: 491 | pass 492 | 493 | try: 494 | root = os.path.realpath(__file__) 495 | # versionfile_source is the relative path from the top of the source 496 | # tree (where the .git directory might live) to this file. Invert 497 | # this to find the root from __file__. 498 | for i in cfg.versionfile_source.split('/'): 499 | root = os.path.dirname(root) 500 | except NameError: 501 | return {"version": "0+unknown", "full-revisionid": None, 502 | "dirty": None, 503 | "error": "unable to find root of source tree", 504 | "date": None} 505 | 506 | try: 507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 | return render(pieces, cfg.style) 509 | except NotThisMethod: 510 | pass 511 | 512 | try: 513 | if cfg.parentdir_prefix: 514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 | except NotThisMethod: 516 | pass 517 | 518 | return {"version": "0+unknown", "full-revisionid": None, 519 | "dirty": None, 520 | "error": "unable to compute version", "date": None} 521 | -------------------------------------------------------------------------------- /src/harvesters_gui/frontend/pyqt5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ---------------------------------------------------------------------------- 3 | # 4 | # Copyright 2018 EMVA 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | 21 | # Standard library imports 22 | import datetime 23 | import os 24 | import sys 25 | import time 26 | 27 | # Related third party imports 28 | from PyQt5.QtCore import QMutexLocker, QMutex, pyqtSignal, QThread 29 | from PyQt5.QtGui import QKeySequence 30 | from PyQt5.QtWidgets import QMainWindow, QAction, QComboBox, \ 31 | QDesktopWidget, QFileDialog, QDialog, QShortcut, QApplication 32 | 33 | from genicam.gentl import NotInitializedException, InvalidHandleException, \ 34 | InvalidIdException, ResourceInUseException, \ 35 | InvalidParameterException, NotImplementedException, \ 36 | AccessDeniedException 37 | 38 | # Local application/library specific imports 39 | from harvesters.core import Harvester as HarvesterCore, ParameterSet, ParameterKey 40 | from harvesters_gui._private.frontend.canvas import Canvas2D 41 | from harvesters_gui._private.frontend.helper import compose_tooltip 42 | from harvesters_gui._private.frontend.pyqt5.about import About 43 | from harvesters_gui._private.frontend.pyqt5.action import Action 44 | from harvesters_gui._private.frontend.pyqt5.attribute_controller import AttributeController 45 | from harvesters_gui._private.frontend.pyqt5.device_list import ComboBoxDeviceList 46 | from harvesters_gui._private.frontend.pyqt5.display_rate_list import ComboBoxDisplayRateList 47 | from harvesters_gui._private.frontend.pyqt5.helper import get_system_font 48 | from harvesters_gui._private.frontend.pyqt5.icon import Icon 49 | from harvesters_gui._private.frontend.pyqt5.thread import _PyQtThread 50 | from harvesters.util.logging import get_logger 51 | 52 | 53 | class Harvester(QMainWindow): 54 | # 55 | _signal_update_statistics = pyqtSignal(str) 56 | _signal_stop_image_acquisition = pyqtSignal() 57 | 58 | def __init__(self, *, vsync=True, logger=None): 59 | # 60 | self._logger = logger or get_logger(name='harvesters') 61 | 62 | # 63 | super().__init__() 64 | 65 | # 66 | self._mutex = QMutex() 67 | 68 | profile = True if 'HARVESTER_PROFILE' in os.environ else False 69 | self._harvester_core = HarvesterCore( 70 | profile=profile, logger=self._logger 71 | ) 72 | self._ia = None # Image Acquirer 73 | 74 | # 75 | self._widget_canvas = Canvas2D(vsync=vsync) 76 | self._widget_canvas.create_native() 77 | self._widget_canvas.native.setParent(self) 78 | 79 | # 80 | self._action_stop_image_acquisition = None 81 | 82 | # 83 | self._observer_widgets = [] 84 | 85 | # 86 | self._widget_device_list = None 87 | self._widget_status_bar = None 88 | self._widget_main = None 89 | self._widget_about = None 90 | self._widget_attribute_controller = None 91 | 92 | # 93 | self._signal_update_statistics.connect(self.update_statistics) 94 | self._signal_stop_image_acquisition.connect(self._stop_image_acquisition) 95 | self._thread_statistics_measurement = _PyQtThread( 96 | parent=self, mutex=self._mutex, 97 | worker=self._worker_update_statistics, 98 | update_cycle_us=250000 99 | ) 100 | 101 | # 102 | self._initialize_widgets() 103 | 104 | # 105 | for o in self._observer_widgets: 106 | o.update() 107 | 108 | def _stop_image_acquisition(self): 109 | self.action_stop_image_acquisition.execute() 110 | 111 | def update_statistics(self, message): 112 | self.statusBar().showMessage(message) 113 | 114 | def closeEvent(self, QCloseEvent): 115 | # 116 | if self._widget_attribute_controller: 117 | self._widget_attribute_controller.close() 118 | 119 | # 120 | if self._harvester_core: 121 | self._harvester_core.reset() 122 | 123 | def __enter__(self): 124 | return self 125 | 126 | def __exit__(self, exc_type, exc_val, exc_tb): 127 | self._harvester_core.reset() 128 | 129 | @property 130 | def canvas(self): 131 | return self._widget_canvas 132 | 133 | @property 134 | def attribute_controller(self): 135 | return self._widget_attribute_controller 136 | 137 | @property 138 | def about(self): 139 | return self._widget_about 140 | 141 | @property 142 | def version(self): 143 | return self.harvester_core.version 144 | 145 | @property 146 | def device_list(self): 147 | return self._widget_device_list 148 | 149 | @property 150 | def cti_files(self): 151 | return self.harvester_core.cti_files 152 | 153 | @property 154 | def harvester_core(self): 155 | return self._harvester_core 156 | 157 | def _initialize_widgets(self): 158 | # 159 | self.setWindowIcon(Icon('genicam_logo_i.png')) 160 | 161 | # 162 | self.setWindowTitle('GenICam.Harvester') 163 | self.setFont(get_system_font()) 164 | 165 | # 166 | self.statusBar().showMessage('') 167 | self.statusBar().setFont(get_system_font()) 168 | 169 | # 170 | self._initialize_gui_toolbar(self._observer_widgets) 171 | 172 | # 173 | self.setCentralWidget(self.canvas.native) 174 | 175 | # 176 | self.resize(800, 600) 177 | 178 | # Place it in the center. 179 | rectangle = self.frameGeometry() 180 | coordinate = QDesktopWidget().availableGeometry().center() 181 | rectangle.moveCenter(coordinate) 182 | self.move(rectangle.topLeft()) 183 | 184 | def _initialize_gui_toolbar(self, observers): 185 | # 186 | group_gentl_info = self.addToolBar('GenTL Information') 187 | group_connection = self.addToolBar('Connection') 188 | group_device = self.addToolBar('Image Acquisition') 189 | group_display = self.addToolBar('Display') 190 | group_help = self.addToolBar('Help') 191 | 192 | # Create buttons: 193 | 194 | # 195 | button_select_file = ActionSelectFile( 196 | icon='open_file.png', title='Select file', parent=self, 197 | action=self.action_on_select_file, 198 | is_enabled=self.is_enabled_on_select_file 199 | ) 200 | shortcut_key = 'Ctrl+o' 201 | button_select_file.setToolTip( 202 | compose_tooltip('Open a CTI file to load', shortcut_key) 203 | ) 204 | button_select_file.setShortcut(shortcut_key) 205 | button_select_file.toggle() 206 | observers.append(button_select_file) 207 | 208 | # 209 | button_update = ActionUpdateList( 210 | icon='update.png', title='Update device list', parent=self, 211 | action=self.action_on_update_list, 212 | is_enabled=self.is_enabled_on_update_list 213 | ) 214 | shortcut_key = 'Ctrl+u' 215 | button_update.setToolTip( 216 | compose_tooltip('Update the device list', shortcut_key) 217 | ) 218 | button_update.setShortcut(shortcut_key) 219 | button_update.toggle() 220 | observers.append(button_update) 221 | 222 | # 223 | button_connect = ActionConnect( 224 | icon='connect.png', title='Connect', parent=self, 225 | action=self.action_on_connect, 226 | is_enabled=self.is_enabled_on_connect 227 | ) 228 | shortcut_key = 'Ctrl+c' 229 | button_connect.setToolTip( 230 | compose_tooltip( 231 | 'Connect the selected device to Harvester', 232 | shortcut_key 233 | ) 234 | ) 235 | button_connect.setShortcut(shortcut_key) 236 | button_connect.toggle() 237 | observers.append(button_connect) 238 | 239 | # 240 | button_disconnect = ActionDisconnect( 241 | icon='disconnect.png', title='Disconnect', parent=self, 242 | action=self.action_on_disconnect, 243 | is_enabled=self.is_enabled_on_disconnect 244 | ) 245 | shortcut_key = 'Ctrl+d' 246 | button_disconnect.setToolTip( 247 | compose_tooltip( 248 | 'Disconnect the device from Harvester', 249 | shortcut_key 250 | ) 251 | ) 252 | button_disconnect.setShortcut(shortcut_key) 253 | button_disconnect.toggle() 254 | observers.append(button_disconnect) 255 | 256 | # 257 | button_start_image_acquisition = ActionStartImageAcquisition( 258 | icon='start_acquisition.png', title='Start Acquisition', parent=self, 259 | action=self.action_on_start_image_acquisition, 260 | is_enabled=self.is_enabled_on_start_image_acquisition 261 | ) 262 | shortcut_key = 'Ctrl+j' 263 | button_start_image_acquisition.setToolTip( 264 | compose_tooltip('Start image acquisition', shortcut_key) 265 | ) 266 | button_start_image_acquisition.setShortcut(shortcut_key) 267 | button_start_image_acquisition.toggle() 268 | observers.append(button_start_image_acquisition) 269 | 270 | # 271 | button_toggle_drawing = ActionToggleDrawing( 272 | icon='pause.png', title='Pause/Resume Drawing', parent=self, 273 | action=self.action_on_toggle_drawing, 274 | is_enabled=self.is_enabled_on_toggle_drawing 275 | ) 276 | shortcut_key = 'Ctrl+k' 277 | button_toggle_drawing.setToolTip( 278 | compose_tooltip('Pause/Resume drawing', shortcut_key) 279 | ) 280 | button_toggle_drawing.setShortcut(shortcut_key) 281 | button_toggle_drawing.toggle() 282 | observers.append(button_toggle_drawing) 283 | 284 | # 285 | button_stop_image_acquisition = ActionStopImageAcquisition( 286 | icon='stop_acquisition.png', title='Stop Acquisition', parent=self, 287 | action=self.action_on_stop_image_acquisition, 288 | is_enabled=self.is_enabled_on_stop_image_acquisition 289 | ) 290 | shortcut_key = 'Ctrl+l' 291 | button_stop_image_acquisition.setToolTip( 292 | compose_tooltip('Stop image acquisition', shortcut_key) 293 | ) 294 | button_stop_image_acquisition.setShortcut(shortcut_key) 295 | button_stop_image_acquisition.toggle() 296 | observers.append(button_stop_image_acquisition) 297 | self._action_stop_image_acquisition = button_stop_image_acquisition 298 | 299 | # 300 | button_dev_attribute = ActionShowAttributeController( 301 | icon='device_attribute.png', title='Device Attribute', parent=self, 302 | action=self.action_on_show_attribute_controller, 303 | is_enabled=self.is_enabled_on_show_attribute_controller 304 | ) 305 | shortcut_key = 'Ctrl+a' 306 | button_dev_attribute.setToolTip( 307 | compose_tooltip('Edit device attribute', shortcut_key) 308 | ) 309 | button_dev_attribute.setShortcut(shortcut_key) 310 | button_dev_attribute.toggle() 311 | observers.append(button_dev_attribute) 312 | 313 | # Create widgets to add: 314 | 315 | # 316 | self._widget_device_list = ComboBoxDeviceList(self) 317 | self._widget_device_list.setSizeAdjustPolicy( 318 | QComboBox.AdjustToContents 319 | ) 320 | shortcut_key = 'Ctrl+Shift+d' 321 | shortcut = QShortcut(QKeySequence(shortcut_key), self) 322 | 323 | def show_popup(): 324 | self._widget_device_list.showPopup() 325 | 326 | shortcut.activated.connect(show_popup) 327 | self._widget_device_list.setToolTip( 328 | compose_tooltip('Select a device to connect', shortcut_key) 329 | ) 330 | observers.append(self._widget_device_list) 331 | for d in self.harvester_core.device_info_list: 332 | self._widget_device_list.addItem(d) 333 | group_connection.addWidget(self._widget_device_list) 334 | observers.append(self._widget_device_list) 335 | 336 | # 337 | self._widget_display_rates = ComboBoxDisplayRateList(self) 338 | self._widget_display_rates.setSizeAdjustPolicy( 339 | QComboBox.AdjustToContents 340 | ) 341 | shortcut_key = 'Ctrl+Shift+r' 342 | shortcut = QShortcut(QKeySequence(shortcut_key), self) 343 | 344 | def show_popup(): 345 | self._widget_display_rates.showPopup() 346 | 347 | shortcut.activated.connect(show_popup) 348 | self._widget_display_rates.setToolTip( 349 | compose_tooltip('Select a display rate', shortcut_key) 350 | ) 351 | observers.append(self._widget_display_rates) 352 | self._widget_display_rates.setEnabled(True) 353 | group_display.addWidget(self._widget_display_rates) 354 | observers.append(self._widget_display_rates) 355 | 356 | # 357 | self._widget_about = About(self) 358 | button_about = ActionShowAbout( 359 | icon='about.png', title='About', parent=self, 360 | action=self.action_on_show_about 361 | ) 362 | button_about.setToolTip( 363 | compose_tooltip('Show information about Harvester') 364 | ) 365 | button_about.toggle() 366 | observers.append(button_about) 367 | 368 | # Configure observers: 369 | 370 | # 371 | button_select_file.add_observer(button_update) 372 | button_select_file.add_observer(button_connect) 373 | button_select_file.add_observer(button_disconnect) 374 | button_select_file.add_observer(button_dev_attribute) 375 | button_select_file.add_observer(button_start_image_acquisition) 376 | button_select_file.add_observer(button_toggle_drawing) 377 | button_select_file.add_observer(button_stop_image_acquisition) 378 | button_select_file.add_observer(self._widget_device_list) 379 | 380 | # 381 | button_update.add_observer(self._widget_device_list) 382 | button_update.add_observer(button_connect) 383 | 384 | # 385 | button_connect.add_observer(button_select_file) 386 | button_connect.add_observer(button_update) 387 | button_connect.add_observer(button_disconnect) 388 | button_connect.add_observer(button_dev_attribute) 389 | button_connect.add_observer(button_start_image_acquisition) 390 | button_connect.add_observer(button_toggle_drawing) 391 | button_connect.add_observer(button_stop_image_acquisition) 392 | button_connect.add_observer(self._widget_device_list) 393 | 394 | # 395 | button_disconnect.add_observer(button_select_file) 396 | button_disconnect.add_observer(button_update) 397 | button_disconnect.add_observer(button_connect) 398 | button_disconnect.add_observer(button_dev_attribute) 399 | button_disconnect.add_observer(button_start_image_acquisition) 400 | button_disconnect.add_observer(button_toggle_drawing) 401 | button_disconnect.add_observer(button_stop_image_acquisition) 402 | button_disconnect.add_observer(self._widget_device_list) 403 | 404 | # 405 | button_start_image_acquisition.add_observer(button_toggle_drawing) 406 | button_start_image_acquisition.add_observer(button_stop_image_acquisition) 407 | 408 | # 409 | button_toggle_drawing.add_observer(button_start_image_acquisition) 410 | button_toggle_drawing.add_observer(button_stop_image_acquisition) 411 | 412 | # 413 | button_stop_image_acquisition.add_observer(button_start_image_acquisition) 414 | button_stop_image_acquisition.add_observer(button_toggle_drawing) 415 | 416 | # Add buttons to groups: 417 | 418 | # 419 | group_gentl_info.addAction(button_select_file) 420 | group_gentl_info.addAction(button_update) 421 | 422 | # 423 | group_connection.addAction(button_connect) 424 | group_connection.addAction(button_disconnect) 425 | 426 | # 427 | group_device.addAction(button_start_image_acquisition) 428 | group_device.addAction(button_toggle_drawing) 429 | group_device.addAction(button_stop_image_acquisition) 430 | group_device.addAction(button_dev_attribute) 431 | 432 | # 433 | group_help.addAction(button_about) 434 | 435 | # Connect handler functions: 436 | 437 | # 438 | group_gentl_info.actionTriggered[QAction].connect( 439 | self.on_button_clicked_action 440 | ) 441 | group_connection.actionTriggered[QAction].connect( 442 | self.on_button_clicked_action 443 | ) 444 | group_device.actionTriggered[QAction].connect( 445 | self.on_button_clicked_action 446 | ) 447 | group_display.actionTriggered[QAction].connect( 448 | self.on_button_clicked_action 449 | ) 450 | group_help.actionTriggered[QAction].connect( 451 | self.on_button_clicked_action 452 | ) 453 | 454 | @staticmethod 455 | def on_button_clicked_action(action): 456 | action.execute() 457 | 458 | @property 459 | def action_stop_image_acquisition(self): 460 | return self._action_stop_image_acquisition 461 | 462 | @property 463 | def ia(self): 464 | return self._ia 465 | 466 | @ia.setter 467 | def ia(self, value): 468 | self._ia = value 469 | 470 | def action_on_connect(self): 471 | # 472 | config = ParameterSet({ 473 | ParameterKey.THREAD_FACTORY_METHOD: lambda: _PyQtThread( 474 | parent=self, mutex=self._mutex), 475 | }) 476 | try: 477 | self._ia = self.harvester_core.create( 478 | self.device_list.currentIndex(), config=config) 479 | # We want to hold one buffer to keep the chunk data alive: 480 | self._ia.num_buffers += 1 481 | except ( 482 | NotInitializedException, InvalidHandleException, 483 | InvalidIdException, ResourceInUseException, 484 | InvalidParameterException, NotImplementedException, 485 | AccessDeniedException, 486 | ) as e: 487 | self._logger.error(e, exc_info=True) 488 | 489 | if not self._ia: 490 | # The device is not available. 491 | return 492 | 493 | self.ia.signal_stop_image_acquisition = self._signal_stop_image_acquisition 494 | 495 | try: 496 | if self.ia.remote_device.node_map: 497 | self._widget_attribute_controller = \ 498 | AttributeController( 499 | self.ia.remote_device.node_map, 500 | parent=self 501 | ) 502 | except AttributeError: 503 | pass 504 | 505 | # 506 | self.canvas.ia = self.ia 507 | 508 | def is_enabled_on_connect(self): 509 | enable = False 510 | if self.cti_files: 511 | if self.harvester_core.device_info_list: 512 | if self.ia is None: 513 | enable = True 514 | return enable 515 | 516 | def action_on_disconnect(self): 517 | if self.attribute_controller: 518 | if self.attribute_controller.isVisible(): 519 | self.attribute_controller.close() 520 | self._widget_attribute_controller = None 521 | 522 | # Discard the image acquisition manager. 523 | if self.ia: 524 | self.ia.destroy() 525 | self._ia = None 526 | 527 | def action_on_select_file(self): 528 | # Show a dialog and update the CTI file list. 529 | dialog = QFileDialog(self) 530 | dialog.setWindowTitle('Select a CTI file to load') 531 | dialog.setNameFilter('CTI files (*.cti)') 532 | dialog.setFileMode(QFileDialog.ExistingFile) 533 | 534 | if dialog.exec_() == QDialog.Accepted: 535 | # 536 | file_path = dialog.selectedFiles()[0] 537 | 538 | # 539 | self.harvester_core.reset() 540 | 541 | # Update the path to the target GenTL Producer. 542 | self.harvester_core.add_cti_file(file_path) 543 | 544 | # Update the device list. 545 | self.harvester_core.update() 546 | 547 | def is_enabled_on_select_file(self): 548 | enable = False 549 | if self.ia is None: 550 | enable = True 551 | return enable 552 | 553 | def action_on_update_list(self): 554 | self.harvester_core.update() 555 | 556 | def is_enabled_on_update_list(self): 557 | enable = False 558 | if self.cti_files: 559 | if self.ia is None: 560 | enable = True 561 | return enable 562 | 563 | def is_enabled_on_disconnect(self): 564 | enable = False 565 | if self.cti_files: 566 | if self.ia: 567 | enable = True 568 | return enable 569 | 570 | def action_on_start_image_acquisition(self): 571 | if self.ia.is_acquiring(): 572 | # If it's pausing drawing images, just resume it and 573 | # immediately return this method. 574 | if self.canvas.is_pausing(): 575 | self.canvas.resume_drawing() 576 | else: 577 | # Start statistics measurement: 578 | self.ia.statistics.reset() 579 | self._thread_statistics_measurement.start() 580 | 581 | self.ia.start() 582 | 583 | def is_enabled_on_start_image_acquisition(self): 584 | enable = False 585 | if self.cti_files: 586 | if self.ia: 587 | if not self.ia.is_acquiring() or \ 588 | self.canvas.is_pausing(): 589 | enable = True 590 | return enable 591 | 592 | def action_on_stop_image_acquisition(self): 593 | # Stop statistics measurement: 594 | self._thread_statistics_measurement.stop() 595 | 596 | # Release the preserved buffers, which the we kept chunk data alive, 597 | # before stopping image acquisition. Otherwise the preserved buffers 598 | # will be dangling after stopping image acquisition: 599 | self.canvas.release_buffers() 600 | 601 | # Then we stop image acquisition: 602 | self.ia.stop() 603 | 604 | # Initialize the drawing state: 605 | self.canvas.pause_drawing(False) 606 | 607 | def is_enabled_on_stop_image_acquisition(self): 608 | enable = False 609 | if self.cti_files: 610 | if self.ia: 611 | if self.ia.is_acquiring(): 612 | enable = True 613 | return enable 614 | 615 | def action_on_show_attribute_controller(self): 616 | if self.ia and self.attribute_controller.isHidden(): 617 | self.attribute_controller.show() 618 | self.attribute_controller.expand_all() 619 | 620 | def is_enabled_on_show_attribute_controller(self): 621 | enable = False 622 | if self.cti_files: 623 | if self.ia is not None: 624 | enable = True 625 | return enable 626 | 627 | def action_on_toggle_drawing(self): 628 | self.canvas.toggle_drawing() 629 | 630 | def is_enabled_on_toggle_drawing(self): 631 | enable = False 632 | if self.cti_files: 633 | if self.ia: 634 | if self.ia.is_acquiring(): 635 | enable = True 636 | return enable 637 | 638 | def action_on_show_about(self): 639 | self.about.setModal(False) 640 | self.about.show() 641 | 642 | def _worker_update_statistics(self): 643 | # 644 | if self.ia is None: 645 | return 646 | 647 | # 648 | message_config = 'W: {0} x H: {1}, {2}, '.format( 649 | self.ia.remote_device.node_map.Width.value, 650 | self.ia.remote_device.node_map.Height.value, 651 | self.ia.remote_device.node_map.PixelFormat.value 652 | ) 653 | # 654 | message_statistics = '{0:.1f} fps, elapsed {1}, {2} images'.format( 655 | self.ia.statistics.fps, 656 | str(datetime.timedelta( 657 | seconds=int(self.ia.statistics.elapsed_time_s) 658 | )), 659 | self.ia.statistics.num_images 660 | ) 661 | # 662 | self._signal_update_statistics.emit( 663 | message_config + message_statistics 664 | ) 665 | 666 | 667 | class ActionSelectFile(Action): 668 | def __init__( 669 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 670 | ): 671 | # 672 | super().__init__( 673 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 674 | ) 675 | 676 | 677 | class ActionUpdateList(Action): 678 | def __init__( 679 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 680 | ): 681 | # 682 | super().__init__( 683 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 684 | ) 685 | 686 | 687 | class ActionConnect(Action): 688 | def __init__( 689 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 690 | ): 691 | # 692 | super().__init__( 693 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 694 | ) 695 | 696 | 697 | class ActionDisconnect(Action): 698 | def __init__( 699 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 700 | ): 701 | # 702 | super().__init__( 703 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 704 | ) 705 | 706 | 707 | class ActionStartImageAcquisition(Action): 708 | def __init__( 709 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 710 | ): 711 | # 712 | super().__init__( 713 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 714 | ) 715 | 716 | 717 | class ActionToggleDrawing(Action): 718 | def __init__( 719 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 720 | ): 721 | # 722 | super().__init__( 723 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled, 724 | checkable=True 725 | ) 726 | 727 | def _update(self): 728 | # 729 | checked = True if self.parent().canvas.is_pausing() else False 730 | self.setChecked(checked) 731 | 732 | 733 | class ActionStopImageAcquisition(Action): 734 | def __init__( 735 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 736 | ): 737 | # 738 | super().__init__( 739 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 740 | ) 741 | 742 | 743 | class ActionShowAttributeController(Action): 744 | def __init__( 745 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 746 | ): 747 | # 748 | super().__init__( 749 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 750 | ) 751 | 752 | 753 | class ActionShowAbout(Action): 754 | def __init__( 755 | self, icon=None, title=None, parent=None, action=None, is_enabled=None 756 | ): 757 | # 758 | super().__init__( 759 | icon=icon, title=title, parent=parent, action=action, is_enabled=is_enabled 760 | ) 761 | 762 | # 763 | self._is_model = False 764 | 765 | 766 | if __name__ == '__main__': 767 | app = QApplication(sys.argv) 768 | harvester = Harvester(vsync=True) 769 | harvester.show() 770 | sys.exit(app.exec_()) 771 | --------------------------------------------------------------------------------