shinestacker 1.5.3__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- {shinestacker-1.5.3 → shinestacker-1.6.0}/CHANGELOG.md +39 -3
- {shinestacker-1.5.3/src/shinestacker.egg-info → shinestacker-1.6.0}/PKG-INFO +1 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/requirements.txt +0 -1
- shinestacker-1.6.0/src/shinestacker/_version.py +1 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/multilayer.py +1 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/stack.py +17 -9
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/args_parser_opts.py +4 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/gui_utils.py +10 -2
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/main.py +5 -2
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/project.py +4 -2
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/retouch.py +3 -1
- shinestacker-1.6.0/src/shinestacker/app/settings_dialog.py +171 -0
- shinestacker-1.6.0/src/shinestacker/config/app_config.py +30 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/config/constants.py +3 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/config/gui_constants.py +4 -2
- shinestacker-1.6.0/src/shinestacker/config/settings.py +110 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/action_config.py +6 -5
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/action_config_dialog.py +17 -74
- shinestacker-1.6.0/src/shinestacker/gui/config_dialog.py +78 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/main_window.py +16 -6
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/menu_manager.py +16 -10
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/new_project.py +2 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/project_controller.py +11 -6
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/project_model.py +16 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/recent_file_manager.py +3 -21
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/display_manager.py +47 -5
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/image_editor_ui.py +59 -15
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/image_view_status.py +4 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/io_gui_handler.py +3 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/overlaid_view.py +6 -43
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/sidebyside_view.py +45 -41
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/transformation_manager.py +0 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/undo_manager.py +1 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/view_strategy.py +125 -61
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/white_balance_filter.py +5 -6
- {shinestacker-1.5.3 → shinestacker-1.6.0/src/shinestacker.egg-info}/PKG-INFO +1 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker.egg-info/SOURCES.txt +4 -0
- shinestacker-1.5.3/src/shinestacker/_version.py +0 -1
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.coveragerc +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.flake8 +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.github/workflows/ci-multiplatform.yml +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.github/workflows/pylint.yml +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.github/workflows/pypi-publish.yml +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.github/workflows/release.yml +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.gitignore +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.pylintrc +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/.readthedocs.yaml +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/LICENSE +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/MANIFEST.in +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/README.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/THIRD_PARTY_LICENSES.txt +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/alignment.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/api.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/balancing.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/conf.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/focus_stacking.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/gui.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/index.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/job.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/main.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/multilayer.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/noise.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/requirements.txt +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/docs/vignetting.md +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/coffee.gif +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/coffee_stack.jpg +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/extreme-vignetting.jpg +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/flies.gif +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/flies_stack.jpg +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/flow-diagram.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/gui-finder.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/gui-project-new.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/gui-project-run.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/img/gui-retouch.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/index.html +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/pyproject.toml +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/scripts/build_release.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/scripts/git-rev-list.sh +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/scripts/validate-tomli.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/setup.cfg +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/__init__.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/__init__.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/align.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/align_auto.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/align_parallel.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/balance.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/denoise.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/depth_map.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/exif.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/noise_detection.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/pyramid.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/sharpen.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/stack_framework.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/utils.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/vignetting.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/algorithms/white_balance.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/__init__.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/about_dialog.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/help_menu.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/app/open_frames.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/config/__init__.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/config/config.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/core/__init__.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/core/colors.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/core/core_utils.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/core/exceptions.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/core/framework.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/core/logging.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/__init__.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/colors.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/flow_layout.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/folder_file_selection.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/gui_images.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/gui_logging.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/gui_run.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/project_converter.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/project_editor.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/select_path_widget.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/sys_mon.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/tab_widget.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/__init__.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/base_filter.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/brush.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/brush_preview.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/brush_tool.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/exif_data.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/file_loader.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/filter_manager.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/icon_container.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/image_viewer.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/io_manager.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/layer_collection.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker.egg-info/requires.txt +0 -0
- {shinestacker-1.5.3 → shinestacker-1.6.0}/src/shinestacker.egg-info/top_level.txt +0 -0
|
@@ -2,11 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
This page reports the main releases only and the main changes therein.
|
|
4
4
|
|
|
5
|
+
## [v1.6.0] - 2025-09.27
|
|
6
|
+
**Few more features and several fixes**
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- persistent settings dialog to configure app startup options
|
|
10
|
+
- command-line option ```-n``` to prevent opening the "new project" dialog
|
|
11
|
+
- zoom factor display in the status bar
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- ghost brush gradient no longer appears at cursor transitions
|
|
15
|
+
- action and job names are now correctly set in the input dialog
|
|
16
|
+
- image centering fixed in viewport for double-view modes
|
|
17
|
+
- frame highlight works correctly when clicking on a thumbnail
|
|
18
|
+
- exif data is now correctly inserted into stacked output files
|
|
19
|
+
- bug in the retouch undo has been fixed
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- cursor updates are now throttled (~60 fps) to improve responsiveness
|
|
23
|
+
- new projects created via dialog save exif data by default
|
|
24
|
+
|
|
25
|
+
----
|
|
26
|
+
|
|
27
|
+
## [v1.5.4] - 2025-09-23
|
|
28
|
+
**Bug fixes**
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- fixed functionality of layer alphabetic sorting
|
|
32
|
+
- fixed image centering in zoom operations
|
|
33
|
+
- fixed color picker reuse in white-balance filter
|
|
34
|
+
- minor fixes and code cleanup
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- menu actions that require a file are disabled when no file is open
|
|
38
|
+
|
|
39
|
+
----
|
|
40
|
+
|
|
5
41
|
## [v1.5.3] - 2025-09-21
|
|
6
42
|
**Bug fixes**
|
|
7
43
|
|
|
8
44
|
### Fixed
|
|
9
|
-
- fixed
|
|
45
|
+
- fixed a critical bug that caused crash at startup in distrubution package due to a relative import in main app scripts
|
|
10
46
|
- fixed relative import in main app scripts
|
|
11
47
|
|
|
12
48
|
### Changed
|
|
@@ -14,7 +50,7 @@ This page reports the main releases only and the main changes therein.
|
|
|
14
50
|
|
|
15
51
|
---
|
|
16
52
|
|
|
17
|
-
## [v1.5.2] - 2025-09-21
|
|
53
|
+
## [v1.5.2] - 2025-09-21 (⚠️ DEPRECATED — use 1.5.3)
|
|
18
54
|
**Bug fixes**
|
|
19
55
|
|
|
20
56
|
### Fixed
|
|
@@ -26,7 +62,7 @@ This page reports the main releases only and the main changes therein.
|
|
|
26
62
|
|
|
27
63
|
---
|
|
28
64
|
|
|
29
|
-
## [v1.5.1] - 2025-09-20
|
|
65
|
+
## [v1.5.1] - 2025-09-20 (⚠️ DEPRECATED — use 1.5.3)
|
|
30
66
|
**Several bug fixes**
|
|
31
67
|
|
|
32
68
|
### Added
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.6.0'
|
|
@@ -174,7 +174,7 @@ class MultiLayer(TaskBase, ImageSequenceManager):
|
|
|
174
174
|
if self.exif_path == '':
|
|
175
175
|
self.exif_path = job.action_path(0)
|
|
176
176
|
if self.exif_path != '':
|
|
177
|
-
self.exif_path = self.working_path
|
|
177
|
+
self.exif_path = os.path.join(self.working_path, self.exif_path)
|
|
178
178
|
|
|
179
179
|
def run_core(self):
|
|
180
180
|
if isinstance(self.input_full_path(), str):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, R0913, R0917
|
|
2
2
|
import os
|
|
3
|
-
import
|
|
3
|
+
import traceback
|
|
4
4
|
from .. config.constants import constants
|
|
5
5
|
from .. core.framework import TaskBase
|
|
6
6
|
from .. core.colors import color_str
|
|
@@ -34,13 +34,19 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
|
34
34
|
self.sub_message_r(': denoise image')
|
|
35
35
|
stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
|
|
36
36
|
write_img(out_filename, stacked_img)
|
|
37
|
-
if self.exif_path != ''
|
|
38
|
-
self.sub_message_r(': copy exif data')
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
if self.exif_path != '':
|
|
38
|
+
self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
|
|
39
|
+
if not os.path.exists(self.exif_path):
|
|
40
|
+
raise RuntimeError(f"Path {self.exif_path} does not exist.")
|
|
41
|
+
try:
|
|
42
|
+
_dirpath, _, fnames = next(os.walk(self.exif_path))
|
|
43
|
+
fnames = [name for name in fnames if extension_tif_jpg(name)]
|
|
44
|
+
exif_filename = os.path.join(self.exif_path, fnames[0])
|
|
45
|
+
copy_exif_from_file_to_file(exif_filename, out_filename)
|
|
46
|
+
self.sub_message_r(' ' * 60)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
traceback.print_tb(e.__traceback__)
|
|
49
|
+
raise RuntimeError("Can't copy EXIF data") from e
|
|
44
50
|
if self.plot_stack:
|
|
45
51
|
idx_str = f"{self.frame_count + 1:04d}" if self.frame_count >= 0 else ''
|
|
46
52
|
name = f"{self.name}: {self.stack_algo.name()}"
|
|
@@ -51,11 +57,13 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
|
51
57
|
self.frame_count += 1
|
|
52
58
|
|
|
53
59
|
def init(self, job, working_path=''):
|
|
60
|
+
if working_path == '':
|
|
61
|
+
working_path = job.working_path
|
|
54
62
|
ImageSequenceManager.init(self, job)
|
|
55
63
|
if self.exif_path is None:
|
|
56
64
|
self.exif_path = job.action_path(0)
|
|
57
65
|
if self.exif_path != '':
|
|
58
|
-
self.exif_path = working_path
|
|
66
|
+
self.exif_path = os.path.join(working_path, self.exif_path)
|
|
59
67
|
|
|
60
68
|
|
|
61
69
|
def get_bunches(collection, n_frames, n_overlap):
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
def add_project_arguments(parser):
|
|
4
4
|
parser.add_argument('-x', '--expert', action='store_true', help='''
|
|
5
5
|
expert options are visible by default.
|
|
6
|
+
''')
|
|
7
|
+
parser.add_argument('-n', '--no-new-project', dest='new-project',
|
|
8
|
+
action='store_false', default=True, help='''
|
|
9
|
+
Do not open new project dialog at startup (default: open).
|
|
6
10
|
''')
|
|
7
11
|
|
|
8
12
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0116, E0611
|
|
1
|
+
# pylint: disable=C0114, C0116, E0611, R0913, R0917
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
from PySide6.QtCore import QCoreApplication, QProcess
|
|
@@ -6,6 +6,7 @@ from PySide6.QtGui import QAction
|
|
|
6
6
|
from shinestacker.config.constants import constants
|
|
7
7
|
from shinestacker.config.config import config
|
|
8
8
|
from shinestacker.app.about_dialog import show_about_dialog
|
|
9
|
+
from shinestacker.app.settings_dialog import show_settings_dialog
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def disable_macos_special_menu_items():
|
|
@@ -40,11 +41,18 @@ def disable_macos_special_menu_items():
|
|
|
40
41
|
QProcess.startDetached("pkill", ["-u", user, "-f", "SystemUIServer"])
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def fill_app_menu(app, app_menu
|
|
44
|
+
def fill_app_menu(app, app_menu, project_settings, retouch_settings,
|
|
45
|
+
handle_project_config, handle_retouch_config):
|
|
44
46
|
about_action = QAction(f"About {constants.APP_STRING}", app)
|
|
45
47
|
about_action.triggered.connect(lambda: show_about_dialog(app))
|
|
46
48
|
app_menu.addAction(about_action)
|
|
47
49
|
app_menu.addSeparator()
|
|
50
|
+
settings_action = QAction("Settings", app)
|
|
51
|
+
settings_action.triggered.connect(lambda: show_settings_dialog(
|
|
52
|
+
app, project_settings, retouch_settings,
|
|
53
|
+
handle_project_config, handle_retouch_config))
|
|
54
|
+
app_menu.addAction(settings_action)
|
|
55
|
+
app_menu.addSeparator()
|
|
48
56
|
if config.DONT_USE_NATIVE_MENU:
|
|
49
57
|
quit_txt, quit_short = "&Quit", "Ctrl+Q"
|
|
50
58
|
else:
|
|
@@ -140,7 +140,9 @@ class MainApp(QMainWindow):
|
|
|
140
140
|
app_menu.addAction(self.switch_to_project_action)
|
|
141
141
|
app_menu.addAction(self.switch_to_retouch_action)
|
|
142
142
|
app_menu.addSeparator()
|
|
143
|
-
fill_app_menu(self, app_menu
|
|
143
|
+
fill_app_menu(self, app_menu, True, True,
|
|
144
|
+
self.project_window.handle_config,
|
|
145
|
+
self.retouch_window.handle_config)
|
|
144
146
|
return app_menu
|
|
145
147
|
|
|
146
148
|
def quit(self):
|
|
@@ -264,7 +266,8 @@ open retouch window at startup instead of project windows.
|
|
|
264
266
|
main_app.switch_to_retouch()
|
|
265
267
|
else:
|
|
266
268
|
main_app.switch_to_project()
|
|
267
|
-
|
|
269
|
+
if args['new-project']:
|
|
270
|
+
QTimer.singleShot(100, main_app.project_window.project_controller.new_project)
|
|
268
271
|
QTimer.singleShot(100, main_app.setFocus)
|
|
269
272
|
sys.exit(app.exec())
|
|
270
273
|
|
|
@@ -30,7 +30,9 @@ class ProjectApp(MainWindow):
|
|
|
30
30
|
|
|
31
31
|
def create_menu(self):
|
|
32
32
|
app_menu = QMenu(constants.APP_STRING)
|
|
33
|
-
fill_app_menu(self, app_menu
|
|
33
|
+
fill_app_menu(self, app_menu, True, False,
|
|
34
|
+
self.handle_config,
|
|
35
|
+
lambda: None)
|
|
34
36
|
return app_menu
|
|
35
37
|
|
|
36
38
|
def _retouch_callback(self, filename):
|
|
@@ -72,7 +74,7 @@ project filename.
|
|
|
72
74
|
filename = args['filename']
|
|
73
75
|
if filename:
|
|
74
76
|
QTimer.singleShot(100, lambda: window.project_controller.open_project(filename))
|
|
75
|
-
|
|
77
|
+
elif args['new-project']:
|
|
76
78
|
QTimer.singleShot(100, window.project_controller.new_project)
|
|
77
79
|
sys.exit(app.exec())
|
|
78
80
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903, E0611, R0902
|
|
2
|
+
from PySide6.QtCore import Signal
|
|
3
|
+
from PySide6.QtWidgets import QFrame, QLabel, QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
|
|
4
|
+
from .. gui.config_dialog import ConfigDialog
|
|
5
|
+
from .. config.settings import Settings
|
|
6
|
+
from .. config.constants import constants
|
|
7
|
+
from .. config.gui_constants import gui_constants
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SettingsDialog(ConfigDialog):
|
|
11
|
+
update_project_config_requested = Signal()
|
|
12
|
+
update_retouch_config_requested = Signal()
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None, project_settings=True, retouch_settings=True):
|
|
15
|
+
self.project_settings = project_settings
|
|
16
|
+
self.retouch_settings = retouch_settings
|
|
17
|
+
self.settings = Settings.instance()
|
|
18
|
+
self.expert_options = None
|
|
19
|
+
self.combined_actions_max_threads = None
|
|
20
|
+
self.align_frames_max_threads = None
|
|
21
|
+
self.focus_stack_max_threads = None
|
|
22
|
+
self.view_strategy = None
|
|
23
|
+
self.min_mouse_step_brush_fraction = None
|
|
24
|
+
self.paint_refresh_time = None
|
|
25
|
+
self.display_refresh_time = None
|
|
26
|
+
self.cursor_update_time = None
|
|
27
|
+
super().__init__("Settings", parent)
|
|
28
|
+
|
|
29
|
+
def create_form_content(self):
|
|
30
|
+
if self.project_settings:
|
|
31
|
+
self.create_project_settings()
|
|
32
|
+
separator = QFrame()
|
|
33
|
+
separator.setFrameShape(QFrame.HLine)
|
|
34
|
+
separator.setFrameShadow(QFrame.Sunken)
|
|
35
|
+
separator.setLineWidth(1)
|
|
36
|
+
self.container_layout.addRow(separator)
|
|
37
|
+
if self.retouch_settings:
|
|
38
|
+
self.create_retouch_settings()
|
|
39
|
+
|
|
40
|
+
def create_project_settings(self):
|
|
41
|
+
label = QLabel("Project settings")
|
|
42
|
+
label.setStyleSheet("font-weight: bold")
|
|
43
|
+
self.container_layout.addRow(label)
|
|
44
|
+
self.expert_options = QCheckBox()
|
|
45
|
+
self.expert_options.setChecked(self.settings.get('expert_options'))
|
|
46
|
+
self.container_layout.addRow("Expert options:", self.expert_options)
|
|
47
|
+
self.combined_actions_max_threads = QSpinBox()
|
|
48
|
+
self.combined_actions_max_threads.setRange(0, 64)
|
|
49
|
+
self.combined_actions_max_threads.setValue(
|
|
50
|
+
self.settings.get('combined_actions_params')['max_threads'])
|
|
51
|
+
self.container_layout.addRow("Max num. of cores, combined actions:",
|
|
52
|
+
self.combined_actions_max_threads)
|
|
53
|
+
|
|
54
|
+
self.align_frames_max_threads = QSpinBox()
|
|
55
|
+
self.align_frames_max_threads.setRange(0, 64)
|
|
56
|
+
self.align_frames_max_threads.setValue(
|
|
57
|
+
self.settings.get('align_frames_params')['max_threads'])
|
|
58
|
+
self.container_layout.addRow("Max num. of cores, align frames:",
|
|
59
|
+
self.align_frames_max_threads)
|
|
60
|
+
|
|
61
|
+
self.focus_stack_max_threads = QSpinBox()
|
|
62
|
+
self.focus_stack_max_threads.setRange(0, 64)
|
|
63
|
+
self.focus_stack_max_threads.setValue(
|
|
64
|
+
self.settings.get('align_frames_params')['max_threads'])
|
|
65
|
+
self.container_layout.addRow("Max num. of cores, focus stacking:",
|
|
66
|
+
self.focus_stack_max_threads)
|
|
67
|
+
|
|
68
|
+
def create_retouch_settings(self):
|
|
69
|
+
label = QLabel("Retouch settings")
|
|
70
|
+
label.setStyleSheet("font-weight: bold")
|
|
71
|
+
self.container_layout.addRow(label)
|
|
72
|
+
self.view_strategy = QComboBox()
|
|
73
|
+
self.view_strategy.addItem("Overlaid", "overlaid")
|
|
74
|
+
self.view_strategy.addItem("Side by side", "sidebyside")
|
|
75
|
+
self.view_strategy.addItem("Top-Bottom", "topbottom")
|
|
76
|
+
idx = self.view_strategy.findData(self.settings.get('view_strategy'))
|
|
77
|
+
if idx >= 0:
|
|
78
|
+
self.view_strategy.setCurrentIndex(idx)
|
|
79
|
+
self.container_layout.addRow("View strategy:", self.view_strategy)
|
|
80
|
+
self.min_mouse_step_brush_fraction = QDoubleSpinBox()
|
|
81
|
+
self.min_mouse_step_brush_fraction.setValue(
|
|
82
|
+
self.settings.get('min_mouse_step_brush_fraction'))
|
|
83
|
+
self.min_mouse_step_brush_fraction.setRange(0, 1)
|
|
84
|
+
self.min_mouse_step_brush_fraction.setDecimals(2)
|
|
85
|
+
self.min_mouse_step_brush_fraction.setSingleStep(0.02)
|
|
86
|
+
self.container_layout.addRow("Min. mouse step in brush units:",
|
|
87
|
+
self.min_mouse_step_brush_fraction)
|
|
88
|
+
self.paint_refresh_time = QSpinBox()
|
|
89
|
+
self.paint_refresh_time.setRange(0, 1000)
|
|
90
|
+
self.paint_refresh_time.setValue(
|
|
91
|
+
self.settings.get('paint_refresh_time'))
|
|
92
|
+
self.container_layout.addRow("Paint refresh time:",
|
|
93
|
+
self.paint_refresh_time)
|
|
94
|
+
self.display_refresh_time = QSpinBox()
|
|
95
|
+
self.display_refresh_time.setRange(0, 200)
|
|
96
|
+
self.display_refresh_time.setValue(
|
|
97
|
+
self.settings.get('display_refresh_time'))
|
|
98
|
+
self.container_layout.addRow("Display refresh time:",
|
|
99
|
+
self.display_refresh_time)
|
|
100
|
+
|
|
101
|
+
self.cursor_update_time = QSpinBox()
|
|
102
|
+
self.cursor_update_time.setRange(0, 50)
|
|
103
|
+
self.cursor_update_time.setValue(
|
|
104
|
+
self.settings.get('cursor_update_time'))
|
|
105
|
+
self.container_layout.addRow("Cursor refresh time:",
|
|
106
|
+
self.cursor_update_time)
|
|
107
|
+
|
|
108
|
+
def accept(self):
|
|
109
|
+
if self.project_settings:
|
|
110
|
+
self.settings.set(
|
|
111
|
+
'expert_options', self.expert_options.isChecked())
|
|
112
|
+
self.settings.set(
|
|
113
|
+
'combined_actions_params', {
|
|
114
|
+
'max_threads': self.combined_actions_max_threads.value()
|
|
115
|
+
})
|
|
116
|
+
self.settings.set(
|
|
117
|
+
'align_frames_params', {
|
|
118
|
+
'max_threads': self.align_frames_max_threads.value()
|
|
119
|
+
})
|
|
120
|
+
self.settings.set(
|
|
121
|
+
'focus_stack_params', {
|
|
122
|
+
'max_threads': self.focus_stack_max_threads.value()
|
|
123
|
+
})
|
|
124
|
+
self.settings.set(
|
|
125
|
+
'focus_stack_bunch:params', {
|
|
126
|
+
'max_threads': self.focus_stack_max_threads.value()
|
|
127
|
+
})
|
|
128
|
+
if self.retouch_settings:
|
|
129
|
+
self.settings.set(
|
|
130
|
+
'view_strategy', self.view_strategy.itemData(self.view_strategy.currentIndex()))
|
|
131
|
+
self.settings.set(
|
|
132
|
+
'min_mouse_step_brush_fraction', self.min_mouse_step_brush_fraction.value())
|
|
133
|
+
self.settings.set(
|
|
134
|
+
'paint_refresh_time', self.paint_refresh_time.value())
|
|
135
|
+
self.settings.set(
|
|
136
|
+
'display_refresh_time', self.display_refresh_time.value())
|
|
137
|
+
self.settings.set(
|
|
138
|
+
'cursor_update_time', self.cursor_update_time.value())
|
|
139
|
+
self.settings.update()
|
|
140
|
+
if self.project_settings:
|
|
141
|
+
self.update_project_config_requested.emit()
|
|
142
|
+
if self.retouch_settings:
|
|
143
|
+
self.update_retouch_config_requested.emit()
|
|
144
|
+
super().accept()
|
|
145
|
+
|
|
146
|
+
def reset_to_defaults(self):
|
|
147
|
+
if self.project_settings:
|
|
148
|
+
self.expert_options.setChecked(constants.DEFAULT_EXPERT_OPTIONS)
|
|
149
|
+
self.combined_actions_max_threads.setValue(constants.DEFAULT_MAX_FWK_THREADS)
|
|
150
|
+
self.align_frames_max_threads.setValue(constants.DEFAULT_ALIGN_MAX_THREADS)
|
|
151
|
+
self.focus_stack_max_threads.setValue(constants.DEFAULT_PY_MAX_THREADS)
|
|
152
|
+
if self.retouch_settings:
|
|
153
|
+
idx = self.view_strategy.findData(constants.DEFAULT_VIEW_STRATEGY)
|
|
154
|
+
if idx >= 0:
|
|
155
|
+
self.view_strategy.setCurrentIndex(idx)
|
|
156
|
+
self.min_mouse_step_brush_fraction.setValue(
|
|
157
|
+
gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION)
|
|
158
|
+
self.paint_refresh_time.setValue(
|
|
159
|
+
gui_constants.DEFAULT_PAINT_REFRESH_TIME)
|
|
160
|
+
self.display_refresh_time.setValue(
|
|
161
|
+
gui_constants.DEFAULT_DISPLAY_REFRESH_TIME)
|
|
162
|
+
self.cursor_update_time.setValue(
|
|
163
|
+
gui_constants.DEFAULT_CURSOR_UPDATE_TIME)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def show_settings_dialog(parent, project_settings, retouch_settings,
|
|
167
|
+
handle_project_config, handle_retouch_config):
|
|
168
|
+
dialog = SettingsDialog(parent, project_settings, retouch_settings)
|
|
169
|
+
dialog.update_project_config_requested.connect(handle_project_config)
|
|
170
|
+
dialog.update_retouch_config_requested.connect(handle_retouch_config)
|
|
171
|
+
dialog.exec()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116
|
|
2
|
+
from .settings import Settings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AppConfig:
|
|
6
|
+
_instance = None
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
if AppConfig._instance is not None:
|
|
10
|
+
raise RuntimeError("AppConfig is a singleton.")
|
|
11
|
+
self.config = {}
|
|
12
|
+
Settings.add_observer(self)
|
|
13
|
+
self.update(Settings.instance().settings)
|
|
14
|
+
|
|
15
|
+
def update(self, settings):
|
|
16
|
+
self.config = {**self.config, **settings}
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def instance(cls):
|
|
20
|
+
if cls._instance is None:
|
|
21
|
+
cls._instance = cls()
|
|
22
|
+
return cls._instance
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get(cls, key, default=None):
|
|
26
|
+
return cls.instance().config.get(key, default)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def set(cls, key, value):
|
|
30
|
+
cls.instance().config[key] = value
|
|
@@ -236,6 +236,9 @@ class _Constants:
|
|
|
236
236
|
DEFAULT_PLOT_STACK_BUNCH = True
|
|
237
237
|
DEFAULT_PLOT_STACK = True
|
|
238
238
|
|
|
239
|
+
DEFAULT_EXPERT_OPTIONS = False
|
|
240
|
+
DEFAULT_VIEW_STRATEGY = 'overlaid'
|
|
241
|
+
|
|
239
242
|
def __setattr__aux(self, name, value):
|
|
240
243
|
raise AttributeError(f"Can't reassign constant '{name}'")
|
|
241
244
|
|
|
@@ -32,8 +32,10 @@ class _GuiConstants:
|
|
|
32
32
|
'preview_inner': (255, 255, 255, 150)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION = 0.25
|
|
36
|
+
DEFAULT_PAINT_REFRESH_TIME = 50 # ms
|
|
37
|
+
DEFAULT_DISPLAY_REFRESH_TIME = 50 # ms
|
|
38
|
+
DEFAULT_CURSOR_UPDATE_TIME = 16 # ms
|
|
37
39
|
|
|
38
40
|
THUMB_WIDTH = 120 # px
|
|
39
41
|
THUMB_HEIGHT = 80 # px
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903, E0611
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import traceback
|
|
5
|
+
import jsonpickle
|
|
6
|
+
from PySide6.QtCore import QStandardPaths
|
|
7
|
+
from .. config.constants import constants
|
|
8
|
+
from .. config.gui_constants import gui_constants
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StdPathFile:
|
|
12
|
+
def __init__(self, filename):
|
|
13
|
+
self._config_dir = None
|
|
14
|
+
self.filename = filename
|
|
15
|
+
|
|
16
|
+
def get_config_dir(self):
|
|
17
|
+
if self._config_dir is None:
|
|
18
|
+
config_dir = QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)
|
|
19
|
+
if not config_dir:
|
|
20
|
+
if os.name == 'nt': # Windows
|
|
21
|
+
config_dir = os.path.join(os.environ.get('APPDATA', ''), 'ShineStacker')
|
|
22
|
+
elif os.name == 'posix': # macOS and Linux
|
|
23
|
+
config_dir = os.path.expanduser('~/.config/shinestacker')
|
|
24
|
+
else:
|
|
25
|
+
config_dir = os.path.join(os.path.expanduser('~'), '.shinestacker')
|
|
26
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
27
|
+
self._config_dir = config_dir
|
|
28
|
+
return self._config_dir
|
|
29
|
+
|
|
30
|
+
def get_file_path(self):
|
|
31
|
+
return os.path.join(self.get_config_dir(), self.filename)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT_SETTINGS = {
|
|
35
|
+
'expert_options': constants.DEFAULT_EXPERT_OPTIONS,
|
|
36
|
+
'view_strategy': constants.DEFAULT_VIEW_STRATEGY,
|
|
37
|
+
'paint_refresh_time': gui_constants.DEFAULT_PAINT_REFRESH_TIME,
|
|
38
|
+
'display_refresh_time': gui_constants.DEFAULT_DISPLAY_REFRESH_TIME,
|
|
39
|
+
'cursor_update_time': gui_constants.DEFAULT_CURSOR_UPDATE_TIME,
|
|
40
|
+
'min_mouse_step_brush_fraction': gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION,
|
|
41
|
+
'combined_actions_params': {
|
|
42
|
+
'max_threads': constants.DEFAULT_MAX_FWK_THREADS
|
|
43
|
+
},
|
|
44
|
+
'align_frames_params': {
|
|
45
|
+
'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS
|
|
46
|
+
},
|
|
47
|
+
'focus_stack_params': {
|
|
48
|
+
'max_threads': constants.DEFAULT_PY_MAX_THREADS
|
|
49
|
+
},
|
|
50
|
+
'focus_stack_bunch_params': {
|
|
51
|
+
'max_threads': constants.DEFAULT_PY_MAX_THREADS
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
CURRENT_VERSION = 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Settings(StdPathFile):
|
|
59
|
+
_instance = None
|
|
60
|
+
_observers = []
|
|
61
|
+
|
|
62
|
+
def __init__(self, filename):
|
|
63
|
+
if Settings._instance is not None:
|
|
64
|
+
raise RuntimeError("Settings is a singleton.")
|
|
65
|
+
super().__init__(filename)
|
|
66
|
+
self.settings = DEFAULT_SETTINGS
|
|
67
|
+
file_path = self.get_file_path()
|
|
68
|
+
if os.path.isfile(file_path):
|
|
69
|
+
try:
|
|
70
|
+
with open(file_path, 'r', encoding="utf-8") as file:
|
|
71
|
+
json_data = json.load(file)
|
|
72
|
+
settings = json_data['settings']
|
|
73
|
+
except Exception as e:
|
|
74
|
+
traceback.print_tb(e.__traceback__)
|
|
75
|
+
print(f"Can't read file from path {file_path}. Default settings ignored.")
|
|
76
|
+
settings = {}
|
|
77
|
+
self.settings = {**self.settings, **settings}
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def instance(cls, filename="shinestacker-settings.txt"):
|
|
81
|
+
if cls._instance is None:
|
|
82
|
+
cls._instance = cls(filename)
|
|
83
|
+
return cls._instance
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def add_observer(cls, observer):
|
|
87
|
+
cls._observers.append(observer)
|
|
88
|
+
|
|
89
|
+
def set(self, key, value):
|
|
90
|
+
self.settings[key] = value
|
|
91
|
+
|
|
92
|
+
def get(self, key, default=None):
|
|
93
|
+
return self.settings.get(key, default)
|
|
94
|
+
|
|
95
|
+
def update(self):
|
|
96
|
+
try:
|
|
97
|
+
config_dir = self.get_config_dir()
|
|
98
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
99
|
+
json_data = {'version': CURRENT_VERSION, 'settings': self.settings}
|
|
100
|
+
json_obj = jsonpickle.encode(json_data)
|
|
101
|
+
with open(self.get_file_path(), 'w', encoding="utf-8") as f:
|
|
102
|
+
f.write(json_obj)
|
|
103
|
+
except IOError as e:
|
|
104
|
+
raise e
|
|
105
|
+
for observer in Settings._observers:
|
|
106
|
+
observer.update(self.settings)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def reset_instance_only_for_testing(cls):
|
|
110
|
+
cls._instance = None
|
|
@@ -49,7 +49,7 @@ class FieldBuilder:
|
|
|
49
49
|
self.fields = {}
|
|
50
50
|
|
|
51
51
|
def add_field(self, tag, field_type, label,
|
|
52
|
-
required=False, add_to_layout=None, **kwargs):
|
|
52
|
+
required=False, add_to_layout=None, do_add=True, **kwargs):
|
|
53
53
|
if field_type == FIELD_TEXT:
|
|
54
54
|
widget = self.create_text_field(tag, **kwargs)
|
|
55
55
|
elif field_type == FIELD_ABS_PATH:
|
|
@@ -99,9 +99,10 @@ class FieldBuilder:
|
|
|
99
99
|
'default_value': default_value,
|
|
100
100
|
**kwargs
|
|
101
101
|
}
|
|
102
|
-
if
|
|
103
|
-
add_to_layout
|
|
104
|
-
|
|
102
|
+
if do_add:
|
|
103
|
+
if add_to_layout is None:
|
|
104
|
+
add_to_layout = self.main_layout
|
|
105
|
+
add_to_layout.addRow(f"{label}:", widget)
|
|
105
106
|
return widget
|
|
106
107
|
|
|
107
108
|
def reset_to_defaults(self):
|
|
@@ -499,7 +500,7 @@ class DefaultActionConfigurator(NoNameActionConfigurator):
|
|
|
499
500
|
name_row = QHBoxLayout()
|
|
500
501
|
name_row.setContentsMargins(0, 0, 0, 0)
|
|
501
502
|
name_label = QLabel(f"{tag} name:")
|
|
502
|
-
name_field = self.
|
|
503
|
+
name_field = self.add_field('name', FIELD_TEXT, f"{tag} name", required=False, do_add=False)
|
|
503
504
|
name_row.addWidget(name_label)
|
|
504
505
|
name_row.addWidget(name_field, 1)
|
|
505
506
|
name_row.addStretch()
|