shinestacker 1.5.3__tar.gz → 1.5.4__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 (151) hide show
  1. {shinestacker-1.5.3 → shinestacker-1.5.4}/CHANGELOG.md +17 -3
  2. {shinestacker-1.5.3/src/shinestacker.egg-info → shinestacker-1.5.4}/PKG-INFO +1 -1
  3. {shinestacker-1.5.3 → shinestacker-1.5.4}/requirements.txt +0 -1
  4. shinestacker-1.5.4/src/shinestacker/_version.py +1 -0
  5. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/main_window.py +10 -0
  6. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/menu_manager.py +14 -10
  7. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/project_controller.py +3 -0
  8. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/image_editor_ui.py +45 -14
  9. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/io_gui_handler.py +3 -0
  10. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/overlaid_view.py +6 -43
  11. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/sidebyside_view.py +17 -29
  12. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/view_strategy.py +111 -57
  13. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/white_balance_filter.py +5 -6
  14. {shinestacker-1.5.3 → shinestacker-1.5.4/src/shinestacker.egg-info}/PKG-INFO +1 -1
  15. shinestacker-1.5.3/src/shinestacker/_version.py +0 -1
  16. {shinestacker-1.5.3 → shinestacker-1.5.4}/.coveragerc +0 -0
  17. {shinestacker-1.5.3 → shinestacker-1.5.4}/.flake8 +0 -0
  18. {shinestacker-1.5.3 → shinestacker-1.5.4}/.github/workflows/ci-multiplatform.yml +0 -0
  19. {shinestacker-1.5.3 → shinestacker-1.5.4}/.github/workflows/pylint.yml +0 -0
  20. {shinestacker-1.5.3 → shinestacker-1.5.4}/.github/workflows/pypi-publish.yml +0 -0
  21. {shinestacker-1.5.3 → shinestacker-1.5.4}/.github/workflows/release.yml +0 -0
  22. {shinestacker-1.5.3 → shinestacker-1.5.4}/.gitignore +0 -0
  23. {shinestacker-1.5.3 → shinestacker-1.5.4}/.pylintrc +0 -0
  24. {shinestacker-1.5.3 → shinestacker-1.5.4}/.readthedocs.yaml +0 -0
  25. {shinestacker-1.5.3 → shinestacker-1.5.4}/LICENSE +0 -0
  26. {shinestacker-1.5.3 → shinestacker-1.5.4}/MANIFEST.in +0 -0
  27. {shinestacker-1.5.3 → shinestacker-1.5.4}/README.md +0 -0
  28. {shinestacker-1.5.3 → shinestacker-1.5.4}/THIRD_PARTY_LICENSES.txt +0 -0
  29. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/alignment.md +0 -0
  30. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/api.md +0 -0
  31. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/balancing.md +0 -0
  32. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/conf.py +0 -0
  33. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/focus_stacking.md +0 -0
  34. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/gui.md +0 -0
  35. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/index.md +0 -0
  36. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/job.md +0 -0
  37. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/main.md +0 -0
  38. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/multilayer.md +0 -0
  39. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/noise.md +0 -0
  40. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/requirements.txt +0 -0
  41. {shinestacker-1.5.3 → shinestacker-1.5.4}/docs/vignetting.md +0 -0
  42. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/coffee.gif +0 -0
  43. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/coffee_stack.jpg +0 -0
  44. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/extreme-vignetting.jpg +0 -0
  45. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/flies.gif +0 -0
  46. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/flies_stack.jpg +0 -0
  47. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/flow-diagram.png +0 -0
  48. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/gui-finder.png +0 -0
  49. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/gui-project-new.png +0 -0
  50. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/gui-project-run.png +0 -0
  51. {shinestacker-1.5.3 → shinestacker-1.5.4}/img/gui-retouch.png +0 -0
  52. {shinestacker-1.5.3 → shinestacker-1.5.4}/index.html +0 -0
  53. {shinestacker-1.5.3 → shinestacker-1.5.4}/pyproject.toml +0 -0
  54. {shinestacker-1.5.3 → shinestacker-1.5.4}/scripts/build_release.py +0 -0
  55. {shinestacker-1.5.3 → shinestacker-1.5.4}/scripts/git-rev-list.sh +0 -0
  56. {shinestacker-1.5.3 → shinestacker-1.5.4}/scripts/validate-tomli.py +0 -0
  57. {shinestacker-1.5.3 → shinestacker-1.5.4}/setup.cfg +0 -0
  58. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/__init__.py +0 -0
  59. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/__init__.py +0 -0
  60. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/align.py +0 -0
  61. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/align_auto.py +0 -0
  62. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/align_parallel.py +0 -0
  63. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/balance.py +0 -0
  64. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  65. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/denoise.py +0 -0
  66. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/depth_map.py +0 -0
  67. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/exif.py +0 -0
  68. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/multilayer.py +0 -0
  69. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/noise_detection.py +0 -0
  70. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/pyramid.py +0 -0
  71. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  72. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  73. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/sharpen.py +0 -0
  74. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/stack.py +0 -0
  75. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/stack_framework.py +0 -0
  76. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/utils.py +0 -0
  77. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/vignetting.py +0 -0
  78. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/algorithms/white_balance.py +0 -0
  79. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/__init__.py +0 -0
  80. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/about_dialog.py +0 -0
  81. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/args_parser_opts.py +0 -0
  82. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/gui_utils.py +0 -0
  83. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/help_menu.py +0 -0
  84. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/main.py +0 -0
  85. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/open_frames.py +0 -0
  86. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/project.py +0 -0
  87. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/app/retouch.py +0 -0
  88. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/config/__init__.py +0 -0
  89. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/config/config.py +0 -0
  90. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/config/constants.py +0 -0
  91. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/config/gui_constants.py +0 -0
  92. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/core/__init__.py +0 -0
  93. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/core/colors.py +0 -0
  94. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/core/core_utils.py +0 -0
  95. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/core/exceptions.py +0 -0
  96. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/core/framework.py +0 -0
  97. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/core/logging.py +0 -0
  98. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/__init__.py +0 -0
  99. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/action_config.py +0 -0
  100. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/action_config_dialog.py +0 -0
  101. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/base_form_dialog.py +0 -0
  102. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/colors.py +0 -0
  103. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/flow_layout.py +0 -0
  104. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/folder_file_selection.py +0 -0
  105. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/gui_images.py +0 -0
  106. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/gui_logging.py +0 -0
  107. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/gui_run.py +0 -0
  108. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  109. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  110. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  111. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  112. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  113. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  114. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  115. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  116. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  117. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/new_project.py +0 -0
  118. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/project_converter.py +0 -0
  119. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/project_editor.py +0 -0
  120. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/project_model.py +0 -0
  121. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/recent_file_manager.py +0 -0
  122. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/select_path_widget.py +0 -0
  123. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/sys_mon.py +0 -0
  124. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/tab_widget.py +0 -0
  125. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/gui/time_progress_bar.py +0 -0
  126. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/__init__.py +0 -0
  127. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/base_filter.py +0 -0
  128. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/brush.py +0 -0
  129. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/brush_gradient.py +0 -0
  130. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/brush_preview.py +0 -0
  131. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/brush_tool.py +0 -0
  132. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/denoise_filter.py +0 -0
  133. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/display_manager.py +0 -0
  134. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/exif_data.py +0 -0
  135. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/file_loader.py +0 -0
  136. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/filter_manager.py +0 -0
  137. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/icon_container.py +0 -0
  138. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/image_view_status.py +0 -0
  139. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/image_viewer.py +0 -0
  140. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/io_manager.py +0 -0
  141. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/layer_collection.py +0 -0
  142. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  143. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/transformation_manager.py +0 -0
  144. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/undo_manager.py +0 -0
  145. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  146. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  147. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker.egg-info/SOURCES.txt +0 -0
  148. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  149. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker.egg-info/entry_points.txt +0 -0
  150. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker.egg-info/requires.txt +0 -0
  151. {shinestacker-1.5.3 → shinestacker-1.5.4}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,11 +2,25 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.5.4] - 2025-09-23
6
+ **Bug fixes**
7
+
8
+ ### Fixed
9
+ - fixed functionality of layer alphabetic sorting
10
+ - fixed image centering in zoom operations
11
+ - fixed color picker reuse in white-balance filter
12
+ - minor fixes and code cleanup
13
+
14
+ ### Changed
15
+ - menu actions that require a file are disabled when no file is open
16
+
17
+ ----
18
+
5
19
  ## [v1.5.3] - 2025-09-21
6
20
  **Bug fixes**
7
21
 
8
22
  ### Fixed
9
- - fixed brush preview visiblity in cursor style transitions
23
+ - fixed a critical bug that caused crash at startup in distrubution package due to a relative import in main app scripts
10
24
  - fixed relative import in main app scripts
11
25
 
12
26
  ### Changed
@@ -14,7 +28,7 @@ This page reports the main releases only and the main changes therein.
14
28
 
15
29
  ---
16
30
 
17
- ## [v1.5.2] - 2025-09-21
31
+ ## [v1.5.2] - 2025-09-21 (⚠️ DEPRECATED — use 1.5.3)
18
32
  **Bug fixes**
19
33
 
20
34
  ### Fixed
@@ -26,7 +40,7 @@ This page reports the main releases only and the main changes therein.
26
40
 
27
41
  ---
28
42
 
29
- ## [v1.5.1] - 2025-09-20
43
+ ## [v1.5.1] - 2025-09-20 (⚠️ DEPRECATED — use 1.5.3)
30
44
  **Several bug fixes**
31
45
 
32
46
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.5.3
3
+ Version: 1.5.4
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -1,5 +1,4 @@
1
1
  matplotlib
2
- matplotlib
3
2
  imagecodecs
4
3
  jsonpickle
5
4
  numpy
@@ -0,0 +1 @@
1
+ __version__ = '1.5.4'
@@ -154,9 +154,14 @@ class MainWindow(QMainWindow, LogManager):
154
154
  self.menu_manager.set_enabled_sub_actions_gui)
155
155
  self.project_controller.add_recent_file_requested.connect(
156
156
  self.menu_manager.add_recent_file)
157
+ self.project_controller.set_enabled_file_open_close_actions_requested.connect(
158
+ self.set_enabled_file_open_close_actions)
159
+
157
160
  self.menu_manager.open_file_requested.connect(
158
161
  self.project_controller.open_project)
159
162
 
163
+ self.set_enabled_file_open_close_actions(False)
164
+
160
165
  def modified(self):
161
166
  return self.project_editor.modified()
162
167
 
@@ -571,3 +576,8 @@ class MainWindow(QMainWindow, LogManager):
571
576
  self.menu_manager.set_enabled_sub_actions_gui(enable_sub_actions)
572
577
  else:
573
578
  self.menu_manager.set_enabled_sub_actions_gui(False)
579
+
580
+ def set_enabled_file_open_close_actions(self, enabled):
581
+ for action in self.findChildren(QAction):
582
+ if action.property("requires_file"):
583
+ action.setEnabled(enabled)
@@ -60,8 +60,10 @@ class MenuManager(QObject):
60
60
  def get_icon(self, icon):
61
61
  return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
62
62
 
63
- def action(self, name):
63
+ def action(self, name, requires_file=False):
64
64
  action = QAction(name, self.parent)
65
+ if requires_file:
66
+ action.setProperty("requires_file", True)
65
67
  shortcut = self.shortcuts.get(name, '')
66
68
  if shortcut:
67
69
  action.setShortcut(shortcut)
@@ -110,20 +112,20 @@ class MenuManager(QObject):
110
112
  self.undo_action.setEnabled(False)
111
113
  menu.addAction(self.undo_action)
112
114
  for name in ["&Cut", "Cop&y", "&Paste", "Duplicate"]:
113
- menu.addAction(self.action(name))
114
- self.delete_element_action = self.action("Delete")
115
+ menu.addAction(self.action(name, requires_file=True))
116
+ self.delete_element_action = self.action("Delete", requires_file=True)
115
117
  self.delete_element_action.setEnabled(False)
116
118
  menu.addAction(self.delete_element_action)
117
119
  menu.addSeparator()
118
120
  for name in ["Move &Up", "Move &Down"]:
119
- menu.addAction(self.action(name))
121
+ menu.addAction(self.action(name, requires_file=True))
120
122
  menu.addSeparator()
121
- self.enable_action = self.action("E&nable")
123
+ self.enable_action = self.action("E&nable", requires_file=True)
122
124
  menu.addAction(self.enable_action)
123
- self.disable_action = self.action("Di&sable")
125
+ self.disable_action = self.action("Di&sable", requires_file=True)
124
126
  menu.addAction(self.disable_action)
125
127
  for name in ["Enable All", "Disable All"]:
126
- menu.addAction(self.action(name))
128
+ menu.addAction(self.action(name, requires_file=True))
127
129
 
128
130
  def add_view_menu(self):
129
131
  menu = self.menubar.addMenu("&View")
@@ -133,13 +135,13 @@ class MenuManager(QObject):
133
135
 
134
136
  def add_job_menu(self):
135
137
  menu = self.menubar.addMenu("&Jobs")
136
- self.add_job_action = self.action("Add Job")
138
+ self.add_job_action = self.action("Add Job", requires_file=True)
137
139
  menu.addAction(self.add_job_action)
138
140
  menu.addSeparator()
139
- self.run_job_action = self.action("Run Job")
141
+ self.run_job_action = self.action("Run Job", requires_file=True)
140
142
  self.run_job_action.setEnabled(False)
141
143
  menu.addAction(self.run_job_action)
142
- self.run_all_jobs_action = self.action("Run All Jobs")
144
+ self.run_all_jobs_action = self.action("Run All Jobs", requires_file=True)
143
145
  self.set_enabled_run_all_jobs(False)
144
146
  menu.addAction(self.run_all_jobs_action)
145
147
 
@@ -148,6 +150,7 @@ class MenuManager(QObject):
148
150
  add_action_menu = QMenu("Add Action", self.parent)
149
151
  for action in constants.ACTION_TYPES:
150
152
  entry_action = QAction(action, self.parent)
153
+ entry_action.setProperty("requires_file", True)
151
154
  entry_action.triggered.connect({
152
155
  constants.ACTION_COMBO: self.add_action_combined_actions,
153
156
  constants.ACTION_NOISEDETECTION: self.add_action_noise_detection,
@@ -161,6 +164,7 @@ class MenuManager(QObject):
161
164
  self.sub_action_menu_entries = []
162
165
  for action in constants.SUB_ACTION_TYPES:
163
166
  entry_action = QAction(action, self.parent)
167
+ entry_action.setProperty("requires_file", True)
164
168
  entry_action.triggered.connect({
165
169
  constants.ACTION_MASKNOISE: self.add_sub_action_make_noise,
166
170
  constants.ACTION_VIGNETTING: self.add_sub_action_vignetting,
@@ -21,6 +21,7 @@ class ProjectController(QObject):
21
21
  enable_save_actions_requested = Signal(bool)
22
22
  enable_sub_actions_requested = Signal(bool)
23
23
  add_recent_file_requested = Signal(str)
24
+ set_enabled_file_open_close_actions_requested = Signal(bool)
24
25
 
25
26
  def __init__(self, parent):
26
27
  super().__init__(parent)
@@ -138,6 +139,7 @@ class ProjectController(QObject):
138
139
  self.clear_action_list()
139
140
  self.mark_as_modified(False)
140
141
  self.project_editor.reset_undo()
142
+ self.set_enabled_file_open_close_actions_requested.emit(False)
141
143
 
142
144
  def new_project(self):
143
145
  if not self.check_unsaved_changes():
@@ -250,6 +252,7 @@ class ProjectController(QObject):
250
252
  project = Project.from_dict(json_obj['project'])
251
253
  if project is None:
252
254
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
255
+ self.set_enabled_file_open_close_actions_requested.emit(True)
253
256
  self.set_project(project)
254
257
  self.mark_as_modified(False)
255
258
  self.add_recent_file_requested.emit(abs_file_path)
@@ -229,6 +229,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
229
229
  self.io_gui_handler.mark_as_modified_requested.connect(self.mark_as_modified)
230
230
  self.io_gui_handler.change_layer_requested.connect(self.change_layer)
231
231
  self.io_gui_handler.add_recent_file_requested.connect(self.add_recent_file)
232
+ self.io_gui_handler.set_enabled_file_open_close_actions_requested.connect(
233
+ self.set_enabled_file_open_close_actions)
232
234
  self.brush_tool.setup_ui(self.brush, self.brush_preview_widget, self.image_viewer,
233
235
  self.brush_size_slider, self.hardness_slider, self.opacity_slider,
234
236
  self.flow_slider)
@@ -277,22 +279,26 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
277
279
 
278
280
  transf_menu = QMenu("&Transform")
279
281
  rotate_90_cw_action = QAction(gui_constants.ROTATE_90_CW_LABEL, self)
280
- transf_menu.addAction(rotate_90_cw_action)
282
+ rotate_90_cw_action.setProperty("requires_file", True)
281
283
  rotate_90_cw_action.triggered.connect(lambda: self.transformation_manager.rotate_90_cw())
284
+ transf_menu.addAction(rotate_90_cw_action)
282
285
  rotate_90_ccw_action = QAction(gui_constants.ROTATE_90_CCW_LABEL, self)
283
- transf_menu.addAction(rotate_90_ccw_action)
286
+ rotate_90_ccw_action.setProperty("requires_file", True)
284
287
  rotate_90_ccw_action.triggered.connect(lambda: self.transformation_manager.rotate_90_ccw())
288
+ transf_menu.addAction(rotate_90_ccw_action)
285
289
  rotate_180_action = QAction(gui_constants.ROTATE_180_LABEL, self)
286
290
  rotate_180_action.triggered.connect(lambda: self.transformation_manager.rotate_180())
291
+ rotate_180_action.setProperty("requires_file", True)
287
292
  transf_menu.addAction(rotate_180_action)
288
293
  edit_menu.addMenu(transf_menu)
289
294
 
290
295
  edit_menu.addSeparator()
291
296
 
292
- copy_action = QAction("Copy Current Layer to Master", self)
293
- copy_action.setShortcut("Ctrl+M")
294
- copy_action.triggered.connect(self.copy_layer_to_master)
295
- edit_menu.addAction(copy_action)
297
+ copy_current_to_master_action = QAction("Copy Current Layer to Master", self)
298
+ copy_current_to_master_action.setShortcut("Ctrl+M")
299
+ copy_current_to_master_action.setProperty("requires_file", True)
300
+ copy_current_to_master_action.triggered.connect(self.copy_layer_to_master)
301
+ edit_menu.addAction(copy_current_to_master_action)
296
302
 
297
303
  view_menu = menubar.addMenu("&View")
298
304
 
@@ -351,15 +357,18 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
351
357
  }
352
358
  brush_action = self.cursor_style_actions['brush']
353
359
  brush_action.setCheckable(True)
360
+ brush_action.setProperty("requires_file", True)
354
361
  brush_action.triggered.connect(lambda: set_cursor_style('brush'))
355
362
  cursor_menu.addAction(brush_action)
356
363
 
357
364
  preview_action = self.cursor_style_actions['preview']
365
+ preview_action.setProperty("requires_file", True)
358
366
  preview_action.setCheckable(True)
359
367
  preview_action.triggered.connect(lambda: set_cursor_style('preview'))
360
368
  cursor_menu.addAction(preview_action)
361
369
 
362
370
  outline_action = self.cursor_style_actions['outline']
371
+ outline_action.setProperty("requires_file", True)
363
372
  outline_action.setCheckable(True)
364
373
  outline_action.triggered.connect(lambda: set_cursor_style('outline'))
365
374
  cursor_menu.addAction(outline_action)
@@ -382,37 +391,44 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
382
391
 
383
392
  zoom_in_action = QAction("Zoom In", self)
384
393
  zoom_in_action.setShortcut("Ctrl++")
394
+ zoom_in_action.setProperty("requires_file", True)
385
395
  zoom_in_action.triggered.connect(self.image_viewer.zoom_in)
386
396
  view_menu.addAction(zoom_in_action)
387
397
 
388
398
  zoom_out_action = QAction("Zoom Out", self)
389
399
  zoom_out_action.setShortcut("Ctrl+-")
400
+ zoom_out_action.setProperty("requires_file", True)
390
401
  zoom_out_action.triggered.connect(self.image_viewer.zoom_out)
391
402
  view_menu.addAction(zoom_out_action)
392
403
 
393
404
  adapt_action = QAction("Adapt to Screen", self)
394
405
  adapt_action.setShortcut("Ctrl+0")
406
+ adapt_action.setProperty("requires_file", True)
395
407
  adapt_action.triggered.connect(self.image_viewer.reset_zoom)
396
408
  view_menu.addAction(adapt_action)
397
409
 
398
410
  actual_size_action = QAction("Actual Size", self)
399
411
  actual_size_action.setShortcut("Ctrl+R")
412
+ actual_size_action.setProperty("requires_file", True)
400
413
  actual_size_action.triggered.connect(self.image_viewer.actual_size)
401
414
  view_menu.addAction(actual_size_action)
402
415
  view_menu.addSeparator()
403
416
 
404
417
  self.view_master_action = QAction("View Master", self)
405
418
  self.view_master_action.setShortcut("M")
419
+ self.view_master_action.setProperty("requires_file", True)
406
420
  self.view_master_action.triggered.connect(self.set_view_master)
407
421
  view_menu.addAction(self.view_master_action)
408
422
 
409
423
  self.view_individual_action = QAction("View Individual", self)
410
424
  self.view_individual_action.setShortcut("L")
425
+ self.view_individual_action.setProperty("requires_file", True)
411
426
  self.view_individual_action.triggered.connect(self.set_view_individual)
412
427
  view_menu.addAction(self.view_individual_action)
413
428
 
414
429
  self.toggle_view_master_individual_action = QAction("Toggle Master/Individual", self)
415
430
  self.toggle_view_master_individual_action.setShortcut("T")
431
+ self.toggle_view_master_individual_action.setProperty("requires_file", True)
416
432
  self.toggle_view_master_individual_action.triggered.connect(
417
433
  self.toggle_view_master_individual)
418
434
  view_menu.addAction(self.toggle_view_master_individual_action)
@@ -421,11 +437,13 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
421
437
  self.set_strategy('overlaid')
422
438
 
423
439
  sort_asc_action = QAction("Sort Layers A-Z", self)
424
- sort_asc_action.triggered.connect(lambda: self.sort_layers('asc'))
440
+ sort_asc_action.setProperty("requires_file", True)
441
+ sort_asc_action.triggered.connect(lambda: self.sort_layers_ui('asc'))
425
442
  view_menu.addAction(sort_asc_action)
426
443
 
427
444
  sort_desc_action = QAction("Sort Layers Z-A", self)
428
- sort_desc_action.triggered.connect(lambda: self.sort_layers('desc'))
445
+ sort_desc_action.setProperty("requires_file", True)
446
+ sort_desc_action.triggered.connect(lambda: self.sort_layers_ui('desc'))
429
447
  view_menu.addAction(sort_desc_action)
430
448
 
431
449
  view_menu.addSeparator()
@@ -433,15 +451,19 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
433
451
  filter_menu = menubar.addMenu("&Filter")
434
452
  filter_menu.setObjectName("Filter")
435
453
  denoise_action = QAction("Denoise", self)
454
+ denoise_action.setProperty("requires_file", True)
436
455
  denoise_action.triggered.connect(self.denoise_filter)
437
456
  filter_menu.addAction(denoise_action)
438
457
  unsharp_mask_action = QAction("Unsharp Mask", self)
458
+ unsharp_mask_action.setProperty("requires_file", True)
439
459
  unsharp_mask_action.triggered.connect(self.unsharp_mask)
440
460
  filter_menu.addAction(unsharp_mask_action)
441
461
  white_balance_action = QAction("White Balance", self)
462
+ white_balance_action.setProperty("requires_file", True)
442
463
  white_balance_action.triggered.connect(self.white_balance)
443
464
  filter_menu.addAction(white_balance_action)
444
465
  vignetting_action = QAction("Vignetting Correction", self)
466
+ vignetting_action.setProperty("requires_file", True)
445
467
  vignetting_action.triggered.connect(self.vignetting_correction)
446
468
  filter_menu.addAction(vignetting_action)
447
469
 
@@ -462,20 +484,29 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
462
484
  prev_layer.activated.connect(self.prev_layer)
463
485
  next_layer = QShortcut(QKeySequence(Qt.Key_Down), self, context=Qt.ApplicationShortcut)
464
486
  next_layer.activated.connect(self.next_layer)
487
+
488
+ self.set_enabled_file_open_close_actions(False)
465
489
  self.installEventFilter(self)
466
490
 
491
+ def set_enabled_view_toggles(self, enabled):
492
+ self.view_master_action.setEnabled(enabled)
493
+ self.view_individual_action.setEnabled(enabled)
494
+ self.toggle_view_master_individual_action.setEnabled(enabled)
495
+
467
496
  def set_strategy(self, strategy):
468
497
  self.image_viewer.set_strategy(strategy)
469
- enable_shortcuts = strategy == 'overlaid'
470
498
  self.display_manager.view_mode = 'master'
471
499
  self.highlight_master_thumbnail(gui_constants.THUMB_MASTER_HI_COLOR)
472
- self.view_master_action.setEnabled(enable_shortcuts)
473
- self.view_individual_action.setEnabled(enable_shortcuts)
474
- self.toggle_view_master_individual_action.setEnabled(enable_shortcuts)
500
+ self.set_enabled_view_toggles(strategy == 'overlaid')
475
501
  for label, mode in self.view_mode_actions.items():
476
502
  mode.setEnabled(label != strategy)
477
503
  mode.setChecked(label == strategy)
478
504
 
505
+ def set_enabled_file_open_close_actions(self, enabled):
506
+ for action in self.findChildren(QAction):
507
+ if action.property("requires_file"):
508
+ action.setEnabled(enabled)
509
+
479
510
  def update_title(self):
480
511
  title = constants.APP_TITLE
481
512
  if self.io_gui_handler is not None:
@@ -544,10 +575,10 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
544
575
  def save_master_only(self, _checked):
545
576
  self.update_title()
546
577
 
547
- def sort_layers(self, order):
578
+ def sort_layers_ui(self, order):
548
579
  self.sort_layers(order)
549
580
  self.display_manager.update_thumbnails()
550
- self.change_layer(self.current_layer())
581
+ self.change_layer(self.current_layer_idx())
551
582
 
552
583
  def change_layer(self, layer_idx):
553
584
  if 0 <= layer_idx < self.number_of_layers():
@@ -17,6 +17,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
17
17
  mark_as_modified_requested = Signal(bool)
18
18
  change_layer_requested = Signal(int)
19
19
  add_recent_file_requested = Signal(str)
20
+ set_enabled_file_open_close_actions_requested = Signal(bool)
20
21
 
21
22
  def __init__(self, layer_collection, undo_manager, parent):
22
23
  QObject.__init__(self, parent)
@@ -165,6 +166,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
165
166
  self.change_layer_requested.emit(0)
166
167
  self.status_message_requested.emit(message)
167
168
  self.update_title_requested.emit()
169
+ self.set_enabled_file_open_close_actions_requested.emit(True)
168
170
  self.add_recent_file_requested.emit(self.current_file_path_master)
169
171
 
170
172
  def save_file(self):
@@ -277,4 +279,5 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
277
279
  self.display_manager.thumbnail_list.clear()
278
280
  self.display_manager.update_thumbnails()
279
281
  self.update_title_requested.emit()
282
+ self.set_enabled_file_open_close_actions_requested.emit(False)
280
283
  self.status_message_requested.emit("File closed")
@@ -1,6 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, E1101, R0904, R0912, R0914, R0902, E0202
2
2
  from PySide6.QtCore import Qt, QPointF, QEvent, QRectF
3
- from .. config.gui_constants import gui_constants
4
3
  from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
5
4
 
6
5
 
@@ -61,41 +60,9 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
61
60
  self.mouse_release_event(event)
62
61
  super().mouseReleaseEvent(event)
63
62
 
64
- # pylint: enable=R0801
65
63
  def wheelEvent(self, event):
66
- if self.empty() or self.gesture_active:
67
- return
68
- if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
69
- if self.control_pressed:
70
- self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
71
- else:
72
- zoom_in_factor = gui_constants.ZOOM_IN_FACTOR
73
- zoom_out_factor = gui_constants.ZOOM_OUT_FACTOR
74
- current_scale = self.get_current_scale()
75
- if event.angleDelta().y() > 0: # Zoom in
76
- new_scale = current_scale * zoom_in_factor
77
- if new_scale <= self.max_scale():
78
- self.scale(zoom_in_factor, zoom_in_factor)
79
- self.set_zoom_factor(new_scale)
80
- else: # Zoom out
81
- new_scale = current_scale * zoom_out_factor
82
- if new_scale >= self.min_scale():
83
- self.scale(zoom_out_factor, zoom_out_factor)
84
- self.set_zoom_factor(new_scale)
85
- self.update_brush_cursor()
86
- else: # Touchpad event - fallback for systems without gesture recognition
87
- if not self.control_pressed:
88
- delta = event.pixelDelta() or event.angleDelta() / 8
89
- if delta:
90
- self.scroll_view(self, delta.x(), delta.y())
91
- else: # Control + touchpad scroll for zoom
92
- zoom_in = event.angleDelta().y() > 0
93
- if zoom_in:
94
- self.zoom_in()
95
- else:
96
- self.zoom_out()
64
+ self.handle_wheel_event(event)
97
65
  event.accept()
98
- # pylint: disable=R0801
99
66
 
100
67
  def enterEvent(self, event):
101
68
  self.activateWindow()
@@ -114,10 +81,13 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
114
81
  def set_mouse_callbacks(self, callbacks):
115
82
  self.mousePressEvent = callbacks
116
83
 
84
+ def get_view_with_mouse(self, event=None):
85
+ return self
86
+
87
+ # pylint: enable=C0103
117
88
  def show(self):
118
89
  self.show_master()
119
90
  super().show()
120
- # pylint: enable=C0103
121
91
 
122
92
  def event(self, event):
123
93
  if event.type() == QEvent.Gesture:
@@ -126,14 +96,7 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
126
96
 
127
97
  def setup_scene_image(self, pixmap, pixmap_item):
128
98
  self.setSceneRect(QRectF(pixmap.rect()))
129
- img_width, img_height = pixmap.width(), pixmap.height()
130
- self.set_max_min_scales(img_width, img_height)
131
- view_rect = self.viewport().rect()
132
- scale_x = view_rect.width() / img_width
133
- scale_y = view_rect.height() / img_height
134
- scale_factor = min(scale_x, scale_y)
135
- scale_factor = max(self.min_scale(), min(scale_factor, self.max_scale()))
136
- self.set_zoom_factor(scale_factor)
99
+ _img_width, _img_height, scale_factor = self.setup_view_image(self, pixmap)
137
100
  self.resetTransform()
138
101
  self.scale(scale_factor, scale_factor)
139
102
  self.centerOn(pixmap_item)
@@ -3,6 +3,7 @@ import time
3
3
  from PySide6.QtCore import Qt, Signal, QEvent, QRectF
4
4
  from PySide6.QtGui import QCursor
5
5
  from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QFrame
6
+
6
7
  from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
7
8
 
8
9
 
@@ -229,25 +230,20 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
229
230
  self.current_view.mousePressEvent = callbacks
230
231
  # pylint: enable=C0103
231
232
 
232
- # pylint: enable=R0801
233
- def handle_wheel_event(self, event):
234
- if self.empty() or self.gesture_active:
235
- return
236
- if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
237
- if self.control_pressed:
238
- self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
239
- else:
240
- if event.angleDelta().y() > 0: # Zoom in
241
- self.zoom_in()
242
- else: # Zoom out
243
- self.zoom_out()
244
- else: # Touchpad event - handle scrolling
245
- if not self.control_pressed:
246
- delta = event.pixelDelta() or event.angleDelta() / 8
247
- if delta:
248
- self.scroll_view(self.master_view, delta.x(), delta.y())
249
- self.scroll_view(self.current_view, delta.x(), delta.y())
250
- # pylint: disable=R0801
233
+ def get_view_with_mouse(self, event=None):
234
+ if event is None:
235
+ mouse_pos_global = QCursor.pos()
236
+ else:
237
+ mouse_pos_global = event.globalPosition().toPoint()
238
+ mouse_pos_current = self.current_view.mapFromGlobal(mouse_pos_global)
239
+ mouse_pos_master = self.master_view.mapFromGlobal(mouse_pos_global)
240
+ current_has_mouse = self.current_view.rect().contains(mouse_pos_current)
241
+ master_has_mouse = self.master_view.rect().contains(mouse_pos_master)
242
+ if master_has_mouse:
243
+ return self.master_view
244
+ if current_has_mouse:
245
+ return self.current_view
246
+ return None
251
247
 
252
248
  def _apply_zoom_to_view(self, view, factor):
253
249
  view.scale(factor, factor)
@@ -383,14 +379,7 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
383
379
  pixmap = self.status.pixmap_master
384
380
  self.master_view.setSceneRect(QRectF(pixmap.rect()))
385
381
  self.pixmap_item_master.setPixmap(pixmap)
386
- img_width, img_height = pixmap.width(), pixmap.height()
387
- self.set_max_min_scales(img_width, img_height)
388
- view_rect = self.master_view.viewport().rect()
389
- scale_x = view_rect.width() / img_width
390
- scale_y = view_rect.height() / img_height
391
- scale_factor = min(scale_x, scale_y)
392
- scale_factor = max(self.min_scale(), min(scale_factor, self.max_scale()))
393
- self.set_zoom_factor(scale_factor)
382
+ img_width, img_height, scale_factor = self.setup_view_image(self.master_view, pixmap)
394
383
  self.master_view.resetTransform()
395
384
  self.master_view.scale(scale_factor, scale_factor)
396
385
  self.master_view.centerOn(self.pixmap_item_master)
@@ -439,8 +428,7 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
439
428
  size = self.brush.size
440
429
  radius = size / 2
441
430
  self.current_brush_cursor.setRect(
442
- scene_pos.x() - radius, scene_pos.y() - radius,
443
- size, size)
431
+ scene_pos.x() - radius, scene_pos.y() - radius, size, size)
444
432
  if self.brush_cursor.isVisible():
445
433
  self.update_current_cursor_color()
446
434
  self.current_brush_cursor.show()