shinestacker 1.5.4__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.

Files changed (155) hide show
  1. {shinestacker-1.5.4 → shinestacker-1.6.0}/CHANGELOG.md +22 -0
  2. {shinestacker-1.5.4/src/shinestacker.egg-info → shinestacker-1.6.0}/PKG-INFO +1 -1
  3. shinestacker-1.6.0/src/shinestacker/_version.py +1 -0
  4. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/multilayer.py +1 -1
  5. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/stack.py +17 -9
  6. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/args_parser_opts.py +4 -0
  7. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/gui_utils.py +10 -2
  8. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/main.py +5 -2
  9. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/project.py +4 -2
  10. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/retouch.py +3 -1
  11. shinestacker-1.6.0/src/shinestacker/app/settings_dialog.py +171 -0
  12. shinestacker-1.6.0/src/shinestacker/config/app_config.py +30 -0
  13. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/config/constants.py +3 -0
  14. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/config/gui_constants.py +4 -2
  15. shinestacker-1.6.0/src/shinestacker/config/settings.py +110 -0
  16. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/action_config.py +6 -5
  17. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/action_config_dialog.py +17 -74
  18. shinestacker-1.6.0/src/shinestacker/gui/config_dialog.py +78 -0
  19. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/main_window.py +6 -6
  20. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/menu_manager.py +2 -0
  21. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/new_project.py +2 -1
  22. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/project_controller.py +8 -6
  23. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/project_model.py +16 -1
  24. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/recent_file_manager.py +3 -21
  25. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/display_manager.py +47 -5
  26. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/image_editor_ui.py +14 -1
  27. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/image_view_status.py +4 -1
  28. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/sidebyside_view.py +29 -13
  29. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/transformation_manager.py +0 -1
  30. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/undo_manager.py +1 -1
  31. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/view_strategy.py +14 -4
  32. {shinestacker-1.5.4 → shinestacker-1.6.0/src/shinestacker.egg-info}/PKG-INFO +1 -1
  33. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker.egg-info/SOURCES.txt +4 -0
  34. shinestacker-1.5.4/src/shinestacker/_version.py +0 -1
  35. {shinestacker-1.5.4 → shinestacker-1.6.0}/.coveragerc +0 -0
  36. {shinestacker-1.5.4 → shinestacker-1.6.0}/.flake8 +0 -0
  37. {shinestacker-1.5.4 → shinestacker-1.6.0}/.github/workflows/ci-multiplatform.yml +0 -0
  38. {shinestacker-1.5.4 → shinestacker-1.6.0}/.github/workflows/pylint.yml +0 -0
  39. {shinestacker-1.5.4 → shinestacker-1.6.0}/.github/workflows/pypi-publish.yml +0 -0
  40. {shinestacker-1.5.4 → shinestacker-1.6.0}/.github/workflows/release.yml +0 -0
  41. {shinestacker-1.5.4 → shinestacker-1.6.0}/.gitignore +0 -0
  42. {shinestacker-1.5.4 → shinestacker-1.6.0}/.pylintrc +0 -0
  43. {shinestacker-1.5.4 → shinestacker-1.6.0}/.readthedocs.yaml +0 -0
  44. {shinestacker-1.5.4 → shinestacker-1.6.0}/LICENSE +0 -0
  45. {shinestacker-1.5.4 → shinestacker-1.6.0}/MANIFEST.in +0 -0
  46. {shinestacker-1.5.4 → shinestacker-1.6.0}/README.md +0 -0
  47. {shinestacker-1.5.4 → shinestacker-1.6.0}/THIRD_PARTY_LICENSES.txt +0 -0
  48. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/alignment.md +0 -0
  49. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/api.md +0 -0
  50. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/balancing.md +0 -0
  51. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/conf.py +0 -0
  52. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/focus_stacking.md +0 -0
  53. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/gui.md +0 -0
  54. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/index.md +0 -0
  55. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/job.md +0 -0
  56. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/main.md +0 -0
  57. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/multilayer.md +0 -0
  58. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/noise.md +0 -0
  59. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/requirements.txt +0 -0
  60. {shinestacker-1.5.4 → shinestacker-1.6.0}/docs/vignetting.md +0 -0
  61. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/coffee.gif +0 -0
  62. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/coffee_stack.jpg +0 -0
  63. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/extreme-vignetting.jpg +0 -0
  64. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/flies.gif +0 -0
  65. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/flies_stack.jpg +0 -0
  66. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/flow-diagram.png +0 -0
  67. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/gui-finder.png +0 -0
  68. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/gui-project-new.png +0 -0
  69. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/gui-project-run.png +0 -0
  70. {shinestacker-1.5.4 → shinestacker-1.6.0}/img/gui-retouch.png +0 -0
  71. {shinestacker-1.5.4 → shinestacker-1.6.0}/index.html +0 -0
  72. {shinestacker-1.5.4 → shinestacker-1.6.0}/pyproject.toml +0 -0
  73. {shinestacker-1.5.4 → shinestacker-1.6.0}/requirements.txt +0 -0
  74. {shinestacker-1.5.4 → shinestacker-1.6.0}/scripts/build_release.py +0 -0
  75. {shinestacker-1.5.4 → shinestacker-1.6.0}/scripts/git-rev-list.sh +0 -0
  76. {shinestacker-1.5.4 → shinestacker-1.6.0}/scripts/validate-tomli.py +0 -0
  77. {shinestacker-1.5.4 → shinestacker-1.6.0}/setup.cfg +0 -0
  78. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/__init__.py +0 -0
  79. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/__init__.py +0 -0
  80. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/align.py +0 -0
  81. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/align_auto.py +0 -0
  82. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/align_parallel.py +0 -0
  83. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/balance.py +0 -0
  84. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  85. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/denoise.py +0 -0
  86. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  87. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/exif.py +0 -0
  88. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/noise_detection.py +0 -0
  89. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/pyramid.py +0 -0
  90. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  91. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  92. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  93. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/stack_framework.py +0 -0
  94. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/utils.py +0 -0
  95. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/vignetting.py +0 -0
  96. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  97. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/__init__.py +0 -0
  98. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/about_dialog.py +0 -0
  99. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/help_menu.py +0 -0
  100. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/app/open_frames.py +0 -0
  101. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/config/__init__.py +0 -0
  102. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/config/config.py +0 -0
  103. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/core/__init__.py +0 -0
  104. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/core/colors.py +0 -0
  105. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/core/core_utils.py +0 -0
  106. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/core/exceptions.py +0 -0
  107. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/core/framework.py +0 -0
  108. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/core/logging.py +0 -0
  109. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/__init__.py +0 -0
  110. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  111. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/colors.py +0 -0
  112. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/flow_layout.py +0 -0
  113. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/folder_file_selection.py +0 -0
  114. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/gui_images.py +0 -0
  115. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/gui_logging.py +0 -0
  116. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/gui_run.py +0 -0
  117. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  118. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  119. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  120. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  121. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  122. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  123. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  124. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  125. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  126. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/project_converter.py +0 -0
  127. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/project_editor.py +0 -0
  128. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  129. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/sys_mon.py +0 -0
  130. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/tab_widget.py +0 -0
  131. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
  132. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/__init__.py +0 -0
  133. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/base_filter.py +0 -0
  134. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/brush.py +0 -0
  135. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  136. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/brush_preview.py +0 -0
  137. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/brush_tool.py +0 -0
  138. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  139. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/exif_data.py +0 -0
  140. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/file_loader.py +0 -0
  141. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  142. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/icon_container.py +0 -0
  143. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/image_viewer.py +0 -0
  144. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/io_gui_handler.py +0 -0
  145. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/io_manager.py +0 -0
  146. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  147. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/overlaid_view.py +0 -0
  148. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  149. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  150. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  151. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  152. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  153. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  154. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker.egg-info/requires.txt +0 -0
  155. {shinestacker-1.5.4 → shinestacker-1.6.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,6 +2,28 @@
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
+
5
27
  ## [v1.5.4] - 2025-09-23
6
28
  **Bug fixes**
7
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.5.4
3
+ Version: 1.6.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -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 + "/" + self.exif_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 numpy as np
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 != '' and stacked_img.dtype == np.uint8:
38
- self.sub_message_r(': copy exif data')
39
- _dirpath, _, fnames = next(os.walk(self.exif_path))
40
- fnames = [name for name in fnames if extension_tif_jpg(name)]
41
- exif_filename = f"{self.exif_path}/{fnames[0]}"
42
- copy_exif_from_file_to_file(exif_filename, out_filename)
43
- self.sub_message_r(' ' * 60)
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 + "/" + self.exif_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
- QTimer.singleShot(100, main_app.project_window.project_controller.new_project)
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
- else:
77
+ elif args['new-project']:
76
78
  QTimer.singleShot(100, window.project_controller.new_project)
77
79
  sys.exit(app.exec())
78
80
 
@@ -25,7 +25,9 @@ class RetouchApp(ImageEditorUI):
25
25
 
26
26
  def create_menu(self):
27
27
  app_menu = QMenu(constants.APP_STRING)
28
- fill_app_menu(self, app_menu)
28
+ fill_app_menu(self, app_menu, False, True,
29
+ lambda: None,
30
+ self.handle_config)
29
31
  return app_menu
30
32
 
31
33
 
@@ -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
- MIN_MOUSE_STEP_BRUSH_FRACTION = 0.25
36
- PAINT_REFRESH_TIMER = 50 # milliseconds
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 add_to_layout is None:
103
- add_to_layout = self.main_layout
104
- add_to_layout.addRow(f"{label}:", widget)
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.builder.create_text_field('name', required=True)
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()