shinestacker 1.5.0__tar.gz → 1.5.1__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.0 → shinestacker-1.5.1}/CHANGELOG.md +19 -4
  2. {shinestacker-1.5.0/src/shinestacker.egg-info → shinestacker-1.5.1}/PKG-INFO +1 -1
  3. shinestacker-1.5.1/src/shinestacker/_version.py +1 -0
  4. shinestacker-1.5.1/src/shinestacker/app/args.py +23 -0
  5. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/main.py +17 -8
  6. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/project.py +2 -3
  7. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/retouch.py +8 -4
  8. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/config/gui_constants.py +2 -2
  9. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/new_project.py +17 -14
  10. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/brush_preview.py +29 -15
  11. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/image_editor_ui.py +16 -23
  12. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/image_viewer.py +2 -4
  13. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/io_gui_handler.py +0 -3
  14. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/overlaid_view.py +23 -11
  15. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/shortcuts_help.py +35 -31
  16. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/sidebyside_view.py +40 -75
  17. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/view_strategy.py +107 -28
  18. {shinestacker-1.5.0 → shinestacker-1.5.1/src/shinestacker.egg-info}/PKG-INFO +1 -1
  19. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker.egg-info/SOURCES.txt +1 -0
  20. shinestacker-1.5.0/src/shinestacker/_version.py +0 -1
  21. {shinestacker-1.5.0 → shinestacker-1.5.1}/.coveragerc +0 -0
  22. {shinestacker-1.5.0 → shinestacker-1.5.1}/.flake8 +0 -0
  23. {shinestacker-1.5.0 → shinestacker-1.5.1}/.github/workflows/ci-multiplatform.yml +0 -0
  24. {shinestacker-1.5.0 → shinestacker-1.5.1}/.github/workflows/pylint.yml +0 -0
  25. {shinestacker-1.5.0 → shinestacker-1.5.1}/.github/workflows/pypi-publish.yml +0 -0
  26. {shinestacker-1.5.0 → shinestacker-1.5.1}/.github/workflows/release.yml +0 -0
  27. {shinestacker-1.5.0 → shinestacker-1.5.1}/.gitignore +0 -0
  28. {shinestacker-1.5.0 → shinestacker-1.5.1}/.pylintrc +0 -0
  29. {shinestacker-1.5.0 → shinestacker-1.5.1}/.readthedocs.yaml +0 -0
  30. {shinestacker-1.5.0 → shinestacker-1.5.1}/LICENSE +0 -0
  31. {shinestacker-1.5.0 → shinestacker-1.5.1}/MANIFEST.in +0 -0
  32. {shinestacker-1.5.0 → shinestacker-1.5.1}/README.md +0 -0
  33. {shinestacker-1.5.0 → shinestacker-1.5.1}/THIRD_PARTY_LICENSES.txt +0 -0
  34. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/alignment.md +0 -0
  35. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/api.md +0 -0
  36. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/balancing.md +0 -0
  37. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/conf.py +0 -0
  38. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/focus_stacking.md +0 -0
  39. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/gui.md +0 -0
  40. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/index.md +0 -0
  41. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/job.md +0 -0
  42. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/main.md +0 -0
  43. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/multilayer.md +0 -0
  44. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/noise.md +0 -0
  45. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/requirements.txt +0 -0
  46. {shinestacker-1.5.0 → shinestacker-1.5.1}/docs/vignetting.md +0 -0
  47. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/coffee.gif +0 -0
  48. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/coffee_stack.jpg +0 -0
  49. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/extreme-vignetting.jpg +0 -0
  50. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/flies.gif +0 -0
  51. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/flies_stack.jpg +0 -0
  52. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/flow-diagram.png +0 -0
  53. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/gui-finder.png +0 -0
  54. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/gui-project-new.png +0 -0
  55. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/gui-project-run.png +0 -0
  56. {shinestacker-1.5.0 → shinestacker-1.5.1}/img/gui-retouch.png +0 -0
  57. {shinestacker-1.5.0 → shinestacker-1.5.1}/index.html +0 -0
  58. {shinestacker-1.5.0 → shinestacker-1.5.1}/pyproject.toml +0 -0
  59. {shinestacker-1.5.0 → shinestacker-1.5.1}/requirements.txt +0 -0
  60. {shinestacker-1.5.0 → shinestacker-1.5.1}/scripts/build_release.py +0 -0
  61. {shinestacker-1.5.0 → shinestacker-1.5.1}/scripts/git-rev-list.sh +0 -0
  62. {shinestacker-1.5.0 → shinestacker-1.5.1}/scripts/validate-tomli.py +0 -0
  63. {shinestacker-1.5.0 → shinestacker-1.5.1}/setup.cfg +0 -0
  64. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/__init__.py +0 -0
  65. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/__init__.py +0 -0
  66. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/align.py +0 -0
  67. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/align_auto.py +0 -0
  68. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/align_parallel.py +0 -0
  69. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/balance.py +0 -0
  70. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  71. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/denoise.py +0 -0
  72. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/depth_map.py +0 -0
  73. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/exif.py +0 -0
  74. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/multilayer.py +0 -0
  75. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/noise_detection.py +0 -0
  76. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/pyramid.py +0 -0
  77. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  78. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  79. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/sharpen.py +0 -0
  80. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/stack.py +0 -0
  81. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/stack_framework.py +0 -0
  82. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/utils.py +0 -0
  83. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/vignetting.py +0 -0
  84. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/algorithms/white_balance.py +0 -0
  85. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/__init__.py +0 -0
  86. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/about_dialog.py +0 -0
  87. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/gui_utils.py +0 -0
  88. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/help_menu.py +0 -0
  89. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/app/open_frames.py +0 -0
  90. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/config/__init__.py +0 -0
  91. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/config/config.py +0 -0
  92. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/config/constants.py +0 -0
  93. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/core/__init__.py +0 -0
  94. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/core/colors.py +0 -0
  95. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/core/core_utils.py +0 -0
  96. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/core/exceptions.py +0 -0
  97. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/core/framework.py +0 -0
  98. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/core/logging.py +0 -0
  99. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/__init__.py +0 -0
  100. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/action_config.py +0 -0
  101. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/action_config_dialog.py +0 -0
  102. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/base_form_dialog.py +0 -0
  103. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/colors.py +0 -0
  104. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/flow_layout.py +0 -0
  105. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/folder_file_selection.py +0 -0
  106. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/gui_images.py +0 -0
  107. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/gui_logging.py +0 -0
  108. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/gui_run.py +0 -0
  109. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  110. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  111. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  112. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  113. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  114. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  115. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  116. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  117. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  118. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/main_window.py +0 -0
  119. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/menu_manager.py +0 -0
  120. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/project_controller.py +0 -0
  121. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/project_converter.py +0 -0
  122. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/project_editor.py +0 -0
  123. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/project_model.py +0 -0
  124. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/recent_file_manager.py +0 -0
  125. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/select_path_widget.py +0 -0
  126. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/sys_mon.py +0 -0
  127. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/tab_widget.py +0 -0
  128. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/gui/time_progress_bar.py +0 -0
  129. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/__init__.py +0 -0
  130. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/base_filter.py +0 -0
  131. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/brush.py +0 -0
  132. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/brush_gradient.py +0 -0
  133. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/brush_tool.py +0 -0
  134. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/denoise_filter.py +0 -0
  135. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/display_manager.py +0 -0
  136. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/exif_data.py +0 -0
  137. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/file_loader.py +0 -0
  138. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/filter_manager.py +0 -0
  139. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/icon_container.py +0 -0
  140. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/image_view_status.py +0 -0
  141. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/io_manager.py +0 -0
  142. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/layer_collection.py +0 -0
  143. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/transformation_manager.py +0 -0
  144. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/undo_manager.py +0 -0
  145. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  146. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  147. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  148. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  149. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker.egg-info/entry_points.txt +0 -0
  150. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker.egg-info/requires.txt +0 -0
  151. {shinestacker-1.5.0 → shinestacker-1.5.1}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,20 +2,36 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [V1.5.1] - 2025-09-20
6
+ **Several bug fixes**
7
+
8
+ ### Added
9
+ - new command-line parameters -v1, -v2, -v3, allow different view modes at startup
10
+
11
+ ### Fixed
12
+ - consistent and restyled cursor for current layer view
13
+ - fixed ghost cursors in side-by-side views
14
+ - fixed cursor shift at startup
15
+ - fixed brush preview at image borders
16
+ - fixed lower/upper case GUI labels
17
+ - improved help and description text
18
+
19
+ ---
20
+
5
21
  ## [v1.5.0] - 2025-09-16
6
- **GUI updates and fixes**
22
+ **GUI improvements and fixes**
7
23
 
8
24
  ### Added
9
25
  - implemented image rotation
10
- - dotted cursor in secondary two-image view
11
26
 
12
27
  ### Fixed
13
- - fixed zoom in wheel events for side-by-side view
28
+ - fixed zoom in wheel events for side-by-side views
14
29
  - restored standard cursor in empty retouch views
15
30
  - lower/upper case GUI labels
16
31
 
17
32
  ### Changed
18
33
  - code refactoring and cleanup
34
+ - dotted cursor in secondary two-image view
19
35
 
20
36
  ---
21
37
 
@@ -369,5 +385,4 @@ This release is equivalent to v0.3.2, but resolves a problem for PyPI distributi
369
385
  - several stability improvements
370
386
  - several bug fixes
371
387
 
372
-
373
388
  ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.5.0
3
+ Version: 1.5.1
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.5.1'
@@ -0,0 +1,23 @@
1
+ # pylint: disable=C0114, C0116
2
+
3
+ def add_project_arguments(parser):
4
+ parser.add_argument('-x', '--expert', action='store_true', help='''
5
+ expert options are visible by default.
6
+ ''')
7
+
8
+
9
+ def add_retouch_arguments(parser):
10
+ parser.add_argument('-p', '--path', nargs='?', help='''
11
+ import frames from one or more directories.
12
+ Multiple directories can be specified separated by ';'.
13
+ ''')
14
+ view_group = parser.add_mutually_exclusive_group()
15
+ view_group.add_argument('-v1', '--view-overlaid', action='store_true', help='''
16
+ set overlaid view.
17
+ ''')
18
+ view_group.add_argument('-v2', '--view-side-by-side', action='store_true', help='''
19
+ set side-by-side view.
20
+ ''')
21
+ view_group.add_argument('-v3', '--view-top-bottom', action='store_true', help='''
22
+ set top-bottom view.
23
+ ''')
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
1
+ # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201, R0915, R0912
2
2
  import sys
3
3
  import os
4
4
  import logging
@@ -20,6 +20,7 @@ from shinestacker.app.gui_utils import (
20
20
  disable_macos_special_menu_items, fill_app_menu, set_css_style)
21
21
  from shinestacker.app.help_menu import add_help_action
22
22
  from shinestacker.app.open_frames import open_frames
23
+ from .args import add_project_arguments, add_retouch_arguments
23
24
 
24
25
 
25
26
  class SelectionDialog(QDialog):
@@ -211,19 +212,21 @@ if a single file is specified, it can be either a project or an image.
211
212
  Multiple frames can be specified as a list of files.
212
213
  Multiple files can be specified separated by ';'.
213
214
  ''')
214
- parser.add_argument('-p', '--path', nargs='?', help='''
215
- import frames from one or more directories.
216
- Multiple directories can be specified separated by ';'.
215
+ app_group = parser.add_mutually_exclusive_group()
216
+ app_group.add_argument('-j', '--project', action='store_true', help='''
217
+ open project window at startup instead of project windows (default).
217
218
  ''')
218
- parser.add_argument('-r', '--retouch', action='store_true', help='''
219
+ app_group.add_argument('-r', '--retouch', action='store_true', help='''
219
220
  open retouch window at startup instead of project windows.
220
221
  ''')
221
- parser.add_argument('-x', '--expert', action='store_true', help='''
222
- expert options are visible by default.
223
- ''')
222
+ add_project_arguments(parser)
223
+ add_retouch_arguments(parser)
224
224
  args = vars(parser.parse_args(sys.argv[1:]))
225
225
  filename = args['filename']
226
226
  path = args['path']
227
+ if filename and path:
228
+ print("can't specify both arguments --filename and --path", file=sys.stderr)
229
+ sys.exit(1)
227
230
  setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
228
231
  app = Application(sys.argv)
229
232
  if config.DONT_USE_NATIVE_MENU:
@@ -239,6 +242,12 @@ expert options are visible by default.
239
242
  main_app.activateWindow()
240
243
  if args['expert']:
241
244
  main_app.project_window.set_expert_options()
245
+ if args['view_overlaid']:
246
+ main_app.retouch_window.set_strategy('overlaid')
247
+ elif args['view_side_by_side']:
248
+ main_app.retouch_window.set_strategy('sidebyside')
249
+ elif args['view_top_bottom']:
250
+ main_app.retouch_window.set_strategy('topbottom')
242
251
  if filename:
243
252
  filenames = filename.split(';')
244
253
  filename = filenames[0]
@@ -17,6 +17,7 @@ from shinestacker.gui.main_window import MainWindow
17
17
  from shinestacker.app.gui_utils import (
18
18
  disable_macos_special_menu_items, fill_app_menu, set_css_style)
19
19
  from shinestacker.app.help_menu import add_help_action
20
+ from .args import add_project_arguments
20
21
 
21
22
 
22
23
  class ProjectApp(MainWindow):
@@ -52,9 +53,7 @@ def main():
52
53
  parser.add_argument('-f', '--filename', nargs='?', help='''
53
54
  project filename.
54
55
  ''')
55
- parser.add_argument('-x', '--expert', action='store_true', help='''
56
- expert options are visible by default.
57
- ''')
56
+ add_project_arguments(parser)
58
57
  args = vars(parser.parse_args(sys.argv[1:]))
59
58
  setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
60
59
  app = Application(sys.argv)
@@ -13,6 +13,7 @@ from shinestacker.app.gui_utils import (
13
13
  disable_macos_special_menu_items, fill_app_menu, set_css_style)
14
14
  from shinestacker.app.help_menu import add_help_action
15
15
  from shinestacker.app.open_frames import open_frames
16
+ from .args import add_retouch_arguments
16
17
 
17
18
 
18
19
  class RetouchApp(ImageEditorUI):
@@ -44,10 +45,7 @@ def main():
44
45
  import frames from files.
45
46
  Multiple files can be specified separated by ';'.
46
47
  ''')
47
- parser.add_argument('-p', '--path', nargs='?', help='''
48
- import frames from one or more directories.
49
- Multiple directories can be specified separated by ';'.
50
- ''')
48
+ add_retouch_arguments(parser)
51
49
  args = vars(parser.parse_args(sys.argv[1:]))
52
50
  filename = args['filename']
53
51
  path = args['path']
@@ -65,6 +63,12 @@ Multiple directories can be specified separated by ';'.
65
63
  editor = RetouchApp()
66
64
  app.editor = editor
67
65
  editor.show()
66
+ if args['view_overlaid']:
67
+ editor.set_strategy('overlaid')
68
+ elif args['view_side_by_side']:
69
+ editor.set_strategy('sidebyside')
70
+ elif args['view_top_bottom']:
71
+ editor.set_strategy('topbottom')
68
72
  open_frames(editor, filename, path)
69
73
  sys.exit(app.exec())
70
74
 
@@ -26,7 +26,7 @@ class _GuiConstants:
26
26
  'outer': (255, 0, 0, 200),
27
27
  'inner': (255, 0, 0, 150),
28
28
  'gradient_end': (255, 0, 0, 0),
29
- 'pen': (255, 0, 0, 150),
29
+ 'pen': (255, 0, 0, 200),
30
30
  'preview': (255, 180, 180),
31
31
  'cursor_inner': (255, 0, 0, 120),
32
32
  'preview_inner': (255, 255, 255, 150)
@@ -55,7 +55,7 @@ class _GuiConstants:
55
55
  DEFAULT_BRUSH_OPACITY = 100
56
56
  DEFAULT_BRUSH_FLOW = 100
57
57
  BRUSH_SIZES = {
58
- 'default': 50,
58
+ 'default': 100,
59
59
  'min': 5,
60
60
  'mid': 50,
61
61
  'max': 1000
@@ -108,17 +108,19 @@ class NewProjectDialog(BaseFormDialog):
108
108
  step2_layout.addRow("Vignetting correction:", self.vignetting_correction)
109
109
  step2_layout.addRow(
110
110
  # f" {constants.ACTION_ICONS[constants.ACTION_ALIGNFRAMES]} "
111
- "Align layers:", self.align_frames)
111
+ "Align frames:", self.align_frames)
112
112
  step2_layout.addRow(
113
113
  # f" {constants.ACTION_ICONS[constants.ACTION_BALANCEFRAMES]} "
114
- "Balance layers:", self.balance_frames)
114
+ "Balance frames:", self.balance_frames)
115
115
  step2_layout.addRow(
116
116
  # f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACKBUNCH]} "
117
- "Bunch stack:", self.bunch_stack)
118
- step2_layout.addRow("Bunch frames:", self.bunch_frames)
119
- step2_layout.addRow("Bunch overlap:", self.bunch_overlap)
117
+ "Create bunches:", self.bunch_stack)
118
+ self.bunch_stack.setToolTip("Combine multiple frames into fewer, high-quality "
119
+ "composite frames for easier retouching")
120
+ step2_layout.addRow("Frames per bunch:", self.bunch_frames)
121
+ step2_layout.addRow("Overlap between bunches:", self.bunch_overlap)
120
122
  self.bunches_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
121
- step2_layout.addRow("Number of bunches: ", self.bunches_label)
123
+ step2_layout.addRow("Number of resulting bunches: ", self.bunches_label)
122
124
  if self.expert():
123
125
  step2_layout.addRow(
124
126
  f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
@@ -133,14 +135,14 @@ class NewProjectDialog(BaseFormDialog):
133
135
  if self.expert():
134
136
  step2_layout.addRow(
135
137
  f" {constants.ACTION_ICONS[constants.ACTION_MULTILAYER]} "
136
- "Save multi layer TIFF:", self.multi_layer)
138
+ "Export as multilayer TIFF:", self.multi_layer)
137
139
  step2_group.setLayout(step2_layout)
138
140
  self.form_layout.addRow(step2_group)
139
141
  step3_group = QGroupBox("3) Confirm")
140
142
  step3_layout = QVBoxLayout()
141
143
  step3_layout.setContentsMargins(15, 0, 15, 15)
142
144
  step3_layout.addWidget(
143
- QLabel("Click 🆗 to confirm and prepare the job."))
145
+ QLabel("Click 🆗 to create project with these settings."))
144
146
  step3_layout.addWidget(
145
147
  QLabel("Select: <b>View</b> > <b>Expert options</b> for advanced configuration."))
146
148
  step3_group.setLayout(step3_layout)
@@ -149,6 +151,7 @@ class NewProjectDialog(BaseFormDialog):
149
151
  step4_layout = QHBoxLayout()
150
152
  step4_layout.setContentsMargins(15, 0, 15, 15)
151
153
  step4_layout.addWidget(QLabel("Press ▶️ to run your job."))
154
+ step4_layout.addStretch()
152
155
  icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
153
156
  app_icon = QIcon(icon_path)
154
157
  icon_pixmap = app_icon.pixmap(80, 80)
@@ -293,12 +296,12 @@ class NewProjectDialog(BaseFormDialog):
293
296
  "Processing may require a significant amount "
294
297
  "of memory or I/O buffering.\n\n"
295
298
  "Continue anyway?")
296
- msg.setInformativeText("You may consider to split the processing "
297
- " using a bunch stack to reduce memory usage.\n\n"
298
- '✅ Check the option "Bunch stack".\n\n'
299
- "➡️ Check expert options for the stacking algorithm."
300
- 'Go to "View" > "Expert Options".'
301
- )
299
+ msg.setInformativeText('You may consider creating "bunches" to reduce '
300
+ "the number of frames for retouching.\n\n"
301
+ '✅ Check "Create bunches" to combine frames '
302
+ "into manageable composites.\n\n"
303
+ "➡️ Check expert options for the stacking algorithm.\n\n"
304
+ 'Go to "View" > "Expert Options".')
302
305
  msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
303
306
  msg.setDefaultButton(QMessageBox.Cancel)
304
307
  if msg.exec_() != QMessageBox.Ok:
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, W0718
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, W0718, R0915
2
2
  import traceback
3
3
  import numpy as np
4
4
  from PySide6.QtWidgets import QGraphicsPixmapItem
@@ -72,38 +72,52 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
72
72
  self.hide()
73
73
  return
74
74
  radius = size // 2
75
- x = int(scene_pos.x() - radius + 0.5)
76
- y = int(scene_pos.y() - radius)
75
+ x_center = int(scene_pos.x() + 0.5)
76
+ y_center = int(scene_pos.y() + 0.5)
77
+ x = x_center - radius
78
+ y = y_center - radius
77
79
  w = h = size
78
80
  if not self.valid_current_layer_idx():
79
81
  self.hide()
80
82
  return
81
- layer_area = self.get_layer_area(self.current_layer(), x, y, w, h)
82
- master_area = self.get_layer_area(self.master_layer(), x, y, w, h)
83
+ height, width = self.current_layer().shape[:2]
84
+ visible_x = max(0, x)
85
+ visible_y = max(0, y)
86
+ visible_w = min(width, x + w) - visible_x
87
+ visible_h = min(height, y + h) - visible_y
88
+ if visible_w <= 0 or visible_h <= 0:
89
+ self.hide()
90
+ return
91
+ layer_area = self.get_layer_area(
92
+ self.current_layer(), visible_x, visible_y, visible_w, visible_h)
93
+ master_area = self.get_layer_area(
94
+ self.master_layer(), visible_x, visible_y, visible_w, visible_h)
83
95
  if layer_area is None or master_area is None:
84
96
  self.hide()
85
97
  return
86
- height, width = self.current_layer().shape[:2]
87
98
  full_mask = create_brush_mask(size=size, hardness_percent=self.brush.hardness,
88
99
  opacity_percent=self.brush.opacity)[:, :, np.newaxis]
89
- mask_x_start = max(0, -x) if x < 0 else 0
90
- mask_y_start = max(0, -y) if y < 0 else 0
91
- mask_x_end = size - (max(0, (x + w) - width)) if (x + w) > width else size
92
- mask_y_end = size - (max(0, (y + h) - height)) if (y + h) > height else size
100
+ mask_x_start = max(0, -x)
101
+ mask_y_start = max(0, -y)
102
+ mask_x_end = mask_x_start + visible_w
103
+ mask_y_end = mask_y_start + visible_h
93
104
  mask_area = full_mask[mask_y_start:mask_y_end, mask_x_start:mask_x_end]
94
105
  area = (layer_area * mask_area + master_area * (1 - mask_area)) * 255.0
95
106
  area = area.astype(np.uint8)
96
107
  qimage = QImage(area.data, area.shape[1], area.shape[0],
97
108
  area.strides[0], QImage.Format_RGB888)
98
- mask = QPixmap(w, h)
109
+ mask = QPixmap(visible_w, visible_h)
99
110
  mask.fill(Qt.transparent)
100
111
  painter = QPainter(mask)
101
112
  painter.setPen(Qt.NoPen)
102
113
  painter.setBrush(Qt.black)
103
- painter.drawEllipse(0, 0, w, h)
114
+ center_x_in_visible = x_center - visible_x
115
+ center_y_in_visible = y_center - visible_y
116
+ painter.drawEllipse(
117
+ center_x_in_visible - radius, center_y_in_visible - radius, size, size)
104
118
  painter.end()
105
119
  pixmap = QPixmap.fromImage(qimage)
106
- final_pixmap = QPixmap(w, h)
120
+ final_pixmap = QPixmap(visible_w, visible_h)
107
121
  final_pixmap.fill(Qt.transparent)
108
122
  painter = QPainter(final_pixmap)
109
123
  painter.drawPixmap(0, 0, pixmap)
@@ -111,8 +125,8 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
111
125
  painter.drawPixmap(0, 0, mask)
112
126
  painter.end()
113
127
  self.setPixmap(final_pixmap)
114
- x_start, y_start = max(0, x), max(0, y)
115
- self.setPos(x_start, y_start)
128
+ self.setPos(visible_x, visible_y)
129
+ self.show()
116
130
  except Exception:
117
131
  traceback.print_exc()
118
132
  self.hide()
@@ -49,7 +49,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
49
49
  self.filter_manager.register_filter("White Balance", WhiteBalanceFilter)
50
50
  self.filter_manager.register_filter("Vignetting Correction", VignettingFilter)
51
51
  self.shortcuts_help_dialog = None
52
-
53
52
  self.update_title()
54
53
  self.resize(1400, 900)
55
54
  center = QGuiApplication.primaryScreen().geometry().center()
@@ -68,18 +67,15 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
68
67
  side_layout = QVBoxLayout(side_panel)
69
68
  side_layout.setContentsMargins(0, 0, 0, 0)
70
69
  side_layout.setSpacing(2)
71
-
72
70
  brush_panel = QFrame()
73
71
  brush_panel.setFrameShape(QFrame.StyledPanel)
74
72
  brush_panel.setContentsMargins(0, 0, 0, 0)
75
73
  brush_layout = QVBoxLayout(brush_panel)
76
74
  brush_layout.setContentsMargins(0, 0, 0, 0)
77
75
  brush_layout.setSpacing(2)
78
-
79
76
  brush_label = QLabel("Brush Size")
80
77
  brush_label.setAlignment(Qt.AlignCenter)
81
78
  brush_layout.addWidget(brush_label)
82
-
83
79
  self.brush_size_slider = QSlider(Qt.Horizontal)
84
80
  self.brush_size_slider.setRange(0, gui_constants.BRUSH_SIZE_SLIDER_MAX)
85
81
 
@@ -94,7 +90,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
94
90
 
95
91
  self.brush_size_slider.setValue(brush_size_to_slider(self.brush.size))
96
92
  brush_layout.addWidget(self.brush_size_slider)
97
-
98
93
  hardness_label = QLabel("Brush Hardness")
99
94
  hardness_label.setAlignment(Qt.AlignCenter)
100
95
  brush_layout.addWidget(hardness_label)
@@ -102,7 +97,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
102
97
  self.hardness_slider.setRange(0, 100)
103
98
  self.hardness_slider.setValue(self.brush.hardness)
104
99
  brush_layout.addWidget(self.hardness_slider)
105
-
106
100
  opacity_label = QLabel("Brush Opacity")
107
101
  opacity_label.setAlignment(Qt.AlignCenter)
108
102
  brush_layout.addWidget(opacity_label)
@@ -110,7 +104,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
110
104
  self.opacity_slider.setRange(0, 100)
111
105
  self.opacity_slider.setValue(self.brush.opacity)
112
106
  brush_layout.addWidget(self.opacity_slider)
113
-
114
107
  flow_label = QLabel("Brush Flow")
115
108
  flow_label.setAlignment(Qt.AlignCenter)
116
109
  brush_layout.addWidget(flow_label)
@@ -118,7 +111,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
118
111
  self.flow_slider.setRange(1, 100)
119
112
  self.flow_slider.setValue(self.brush.flow)
120
113
  brush_layout.addWidget(self.flow_slider)
121
-
122
114
  side_layout.addWidget(brush_panel)
123
115
  self.brush_preview_widget = QLabel()
124
116
  self.brush_preview_widget.setContentsMargins(0, 0, 0, 0)
@@ -135,7 +127,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
135
127
  self.brush_preview_widget.setFixedHeight(100)
136
128
  brush_layout.addWidget(self.brush_preview_widget)
137
129
  side_layout.addWidget(brush_panel)
138
-
139
130
  master_label = QLabel("Master")
140
131
  master_label.setStyleSheet("""
141
132
  QLabel {
@@ -332,30 +323,20 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
332
323
  overlaid_mode = self.view_mode_actions['overlaid']
333
324
  overlaid_mode.setShortcut("Ctrl+1")
334
325
  overlaid_mode.setCheckable(True)
335
- overlaid_mode.triggered.connect(lambda: set_strategy('overlaid'))
326
+ overlaid_mode.triggered.connect(lambda: self.set_strategy('overlaid'))
336
327
  view_strategy_menu.addAction(overlaid_mode)
337
328
  side_by_side_mode = self.view_mode_actions['sidebyside']
338
329
  side_by_side_mode.setShortcut("Ctrl+2")
339
330
  side_by_side_mode.setCheckable(True)
340
- side_by_side_mode.triggered.connect(lambda: set_strategy('sidebyside'))
331
+ side_by_side_mode.triggered.connect(lambda: self.set_strategy('sidebyside'))
341
332
  view_strategy_menu.addAction(side_by_side_mode)
342
333
  side_by_side_mode = self.view_mode_actions['topbottom']
343
334
  side_by_side_mode.setShortcut("Ctrl+3")
344
335
  side_by_side_mode.setCheckable(True)
345
- side_by_side_mode.triggered.connect(lambda: set_strategy('topbottom'))
336
+ side_by_side_mode.triggered.connect(lambda: self.set_strategy('topbottom'))
346
337
  view_strategy_menu.addAction(side_by_side_mode)
347
338
  view_menu.addMenu(view_strategy_menu)
348
339
 
349
- def set_strategy(strategy):
350
- self.image_viewer.set_strategy(strategy)
351
- enable_shortcuts = strategy == 'overlaid'
352
- self.view_master_action.setEnabled(enable_shortcuts)
353
- self.view_individual_action.setEnabled(enable_shortcuts)
354
- self.toggle_view_master_individual_action.setEnabled(enable_shortcuts)
355
- for label, mode in self.view_mode_actions.items():
356
- mode.setEnabled(label != strategy)
357
- mode.setChecked(label == strategy)
358
-
359
340
  cursor_menu = view_menu.addMenu("Cursor Style")
360
341
 
361
342
  self.cursor_style_actions = {
@@ -432,7 +413,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
432
413
  view_menu.addAction(self.toggle_view_master_individual_action)
433
414
  view_menu.addSeparator()
434
415
 
435
- set_strategy('overlaid')
416
+ self.set_strategy('overlaid')
436
417
 
437
418
  sort_asc_action = QAction("Sort Layers A-Z", self)
438
419
  sort_asc_action.triggered.connect(lambda: self.sort_layers('asc'))
@@ -463,6 +444,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
463
444
  help_menu.setObjectName("Help")
464
445
  shortcuts_help_action = QAction("Shortcuts and Mouse", self)
465
446
 
447
+ self.statusBar().showMessage("Shine Stacker ready.", 2000)
448
+
466
449
  def shortcuts_help():
467
450
  self.shortcuts_help_dialog = ShortcutsHelp(self)
468
451
  self.shortcuts_help_dialog.exec()
@@ -476,6 +459,16 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
476
459
  next_layer.activated.connect(self.next_layer)
477
460
  self.installEventFilter(self)
478
461
 
462
+ def set_strategy(self, strategy):
463
+ self.image_viewer.set_strategy(strategy)
464
+ enable_shortcuts = strategy == 'overlaid'
465
+ self.view_master_action.setEnabled(enable_shortcuts)
466
+ self.view_individual_action.setEnabled(enable_shortcuts)
467
+ self.toggle_view_master_individual_action.setEnabled(enable_shortcuts)
468
+ for label, mode in self.view_mode_actions.items():
469
+ mode.setEnabled(label != strategy)
470
+ mode.setChecked(label == strategy)
471
+
479
472
  def update_title(self):
480
473
  title = constants.APP_TITLE
481
474
  if self.io_gui_handler is not None:
@@ -87,9 +87,6 @@ class ImageViewer(QWidget):
87
87
  def set_allow_cursor_preview(self, state):
88
88
  self.strategy.set_allow_cursor_preview(state)
89
89
 
90
- def setup_brush_cursor(self):
91
- self.strategy.setup_brush_cursor()
92
-
93
90
  def zoom_in(self):
94
91
  self.strategy.zoom_in()
95
92
 
@@ -109,7 +106,8 @@ class ImageViewer(QWidget):
109
106
  return self.strategy.get_cursor_style()
110
107
 
111
108
  def set_cursor_style(self, style):
112
- self.strategy.set_cursor_style(style)
109
+ for st in self._strategies.values():
110
+ st.set_cursor_style(style)
113
111
 
114
112
  def position_on_image(self, pos):
115
113
  return self.strategy.position_on_image(pos)
@@ -56,8 +56,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
56
56
  self.set_layer_labels(labels)
57
57
  self.set_master_layer(master_layer)
58
58
  self.image_viewer.set_master_image_np(master_layer)
59
- self.image_viewer.show_master()
60
- self.image_viewer.update_master_display()
61
59
  self.undo_manager.reset()
62
60
  self.blank_layer = np.zeros(master_layer.shape[:2])
63
61
  self.finish_loading_setup(f"Loaded: {self.current_file_path()}")
@@ -165,7 +163,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
165
163
  self.display_manager.update_thumbnails()
166
164
  self.mark_as_modified_requested.emit(True)
167
165
  self.change_layer_requested.emit(0)
168
- self.image_viewer.setup_brush_cursor()
169
166
  self.status_message_requested.emit(message)
170
167
  self.update_title_requested.emit()
171
168
  self.add_recent_file_requested.emit(self.current_file_path_master)
@@ -114,27 +114,31 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
114
114
  return self.handle_gesture_event(event)
115
115
  return super().event(event)
116
116
 
117
- def set_master_image(self, qimage):
118
- self.status.set_master_image(qimage)
119
- pixmap = self.status.pixmap_master
117
+ def setup_scene_image(self, pixmap, pixmap_item):
120
118
  self.setSceneRect(QRectF(pixmap.rect()))
121
119
  img_width, img_height = pixmap.width(), pixmap.height()
122
120
  self.set_max_min_scales(img_width, img_height)
123
- self.set_zoom_factor(1.0)
121
+ view_rect = self.viewport().rect()
122
+ scale_x = view_rect.width() / img_width
123
+ scale_y = view_rect.height() / img_height
124
+ scale_factor = min(scale_x, scale_y)
125
+ scale_factor = max(self.min_scale(), min(scale_factor, self.max_scale()))
126
+ self.set_zoom_factor(scale_factor)
124
127
  self.resetTransform()
125
- self.fitInView(self.pixmap_item_master, Qt.KeepAspectRatio)
126
- self.set_zoom_factor(self.get_current_scale())
127
- self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), self.zoom_factor())))
128
- self.scale(self.zoom_factor(), self.zoom_factor())
129
- self.centerOn(self.pixmap_item_master)
128
+ self.scale(scale_factor, scale_factor)
129
+ self.centerOn(pixmap_item)
130
130
  self.center_image(self)
131
131
  self.update_cursor_pen_width()
132
132
 
133
+ def set_master_image(self, qimage):
134
+ self.status.set_master_image(qimage)
135
+ self.setup_scene_image(self.status.pixmap_master, self.pixmap_item_master)
136
+ self.update_master_display()
137
+
133
138
  def set_current_image(self, qimage):
134
139
  self.status.set_current_image(qimage)
135
140
  if self.empty():
136
- self.setSceneRect(QRectF(self.status.pixmap_current.rect()))
137
- self.update_cursor_pen_width()
141
+ self.setup_scene_image(self.status.pixmap_current, self.pixmap_item_current)
138
142
 
139
143
  def setup_brush_cursor(self):
140
144
  super().setup_brush_cursor()
@@ -144,11 +148,19 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
144
148
  self.pixmap_item_master.setVisible(True)
145
149
  self.pixmap_item_current.setVisible(False)
146
150
  self.brush_preview.show()
151
+ if self.brush_cursor:
152
+ self.scene.removeItem(self.brush_cursor)
153
+ self.brush_cursor = self.create_circle(self.scene)
154
+ self.update_brush_cursor()
147
155
 
148
156
  def show_current(self):
149
157
  self.pixmap_item_master.setVisible(False)
150
158
  self.pixmap_item_current.setVisible(True)
151
159
  self.brush_preview.hide()
160
+ if self.brush_cursor:
161
+ self.scene.removeItem(self.brush_cursor)
162
+ self.brush_cursor = self.create_alt_circle(self.scene)
163
+ self.update_brush_cursor()
152
164
 
153
165
  def arrange_images(self):
154
166
  if self.empty():