shinestacker 1.5.0__py3-none-any.whl → 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.5.0'
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
+ ''')
shinestacker/app/main.py CHANGED
@@ -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():
@@ -39,7 +39,7 @@ class ShortcutsHelp(QDialog):
39
39
  ok_button.clicked.connect(self.accept)
40
40
 
41
41
  def add_bold_label(self, layout, label):
42
- label = QLabel(label)
42
+ label = QLabel(f"{label}:")
43
43
  label.setStyleSheet("font-weight: bold")
44
44
  layout.addRow(label)
45
45
 
@@ -47,21 +47,21 @@ class ShortcutsHelp(QDialog):
47
47
  self.main_layout.insertWidget(0, icon_container())
48
48
 
49
49
  shortcuts = {
50
- "M": "show master layer",
51
- "L": "show selected layer",
52
- "T": "toggle master/selected layer",
53
- "X": "temp. toggle between master and source layer",
54
- "↑": "select one layer up",
55
- "↓": "selcet one layer down",
56
- "Ctrl + O": "open file",
57
- "Ctrl + S": "save multilayer tiff",
58
- "Crtl + Z": "undo brush draw",
59
- "Ctrl + M": "copy selected layer to master",
60
- "Ctrl + Cmd + F": "full screen mode",
61
- "Ctrl + +": "zoom in",
62
- "Ctrl + -": "zoom out",
63
- "Ctrl + 0": "adapt to screen",
64
- "Ctrl + R": "actual size"
50
+ "M": "Show master layer",
51
+ "L": "Show selected layer",
52
+ "T": "Toggle master/selected layer",
53
+ "X": "Temporarily toggle between master and source layer",
54
+ "↑": "Select one layer up",
55
+ "↓": "Select one layer down",
56
+ "Ctrl + O": "Open file",
57
+ "Ctrl + S": "Save multilayer tiff",
58
+ "Ctrl + Z": "Undo brush draw",
59
+ "Ctrl + M": "Copy selected layer to master",
60
+ "Ctrl + Cmd + F": "Full screen mode",
61
+ "Ctrl + +": "Zoom in",
62
+ "Ctrl + -": "Zoom out",
63
+ "Ctrl + 0": "Fit to screen",
64
+ "Ctrl + R": "Actual size"
65
65
  }
66
66
 
67
67
  self.add_bold_label(left_layout, "Keyboard Shortcuts")
@@ -69,13 +69,13 @@ class ShortcutsHelp(QDialog):
69
69
  left_layout.addRow(f"<b>{k}</b>", QLabel(v))
70
70
 
71
71
  shortcuts = {
72
- "Ctrl + 1": "view mode: overlaid",
73
- "Ctrl + 2": "view mode: side by side",
74
- "Ctrl + 3": "view mode: top-bottom",
75
- "[": "increase brush size",
76
- "]": "decrease brush size",
77
- "{": "increase brush hardness",
78
- "}": "decrease brush hardness"
72
+ "Ctrl + 1": "View: overlaid",
73
+ "Ctrl + 2": "View: side by side",
74
+ "Ctrl + 3": "View: top-bottom",
75
+ "[": "Increase brush size",
76
+ "]": "Decrease brush size",
77
+ "{": "Increase brush hardness",
78
+ "}": "Decrease brush hardness"
79
79
  }
80
80
 
81
81
  self.add_bold_label(right_layout, "Keyboard Shortcuts")
@@ -83,22 +83,26 @@ class ShortcutsHelp(QDialog):
83
83
  right_layout.addRow(f"<b>{k}</b>", QLabel(v))
84
84
 
85
85
  mouse_controls = {
86
- "Space + Drag": "pan",
87
- "Wheel": "zoom in/out",
88
- "Ctrl + Wheel": "adjust brush size",
89
- "Left Click": "brush action",
86
+ "Space + Drag": "Move",
87
+ "Wheel": "Zoom in/out",
88
+ "Ctrl + Wheel": "Adjust brush size",
89
+ "Left Click": "Use brush to copy from selected layer to master",
90
90
  }
91
91
 
92
+ spacer = QLabel("")
93
+ spacer.setFixedHeight(10)
94
+ right_layout.addWidget(spacer)
92
95
  self.add_bold_label(right_layout, "Mouse Controls")
93
96
  for k, v in mouse_controls.items():
94
97
  right_layout.addRow(f"<b>{k}</b>", QLabel(v))
95
98
 
96
99
  touchpad_controls = {
97
- "Two fingers": "pan",
98
- "Pinch": "zoom in/out",
99
- "Ctrl + two fingers": "zoom in/out",
100
+ "Two-finger drag": "Move",
101
+ "Pinch two fingers": "Zoom in/out"
100
102
  }
101
- self.add_bold_label(right_layout, " ")
103
+ spacer = QLabel("")
104
+ spacer.setFixedHeight(10)
105
+ right_layout.addWidget(spacer)
102
106
  self.add_bold_label(right_layout, "Touchpad Controls")
103
107
  for k, v in touchpad_controls.items():
104
108
  right_layout.addRow(f"<b>{k}</b>", QLabel(v))
@@ -1,8 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003
2
2
  from PySide6.QtCore import Qt, Signal, QEvent, QRectF
3
- from PySide6.QtGui import QPen, QColor, QCursor
4
- from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QFrame, QGraphicsEllipseItem
5
- from .. config.gui_constants import gui_constants
3
+ from PySide6.QtGui import QCursor
4
+ from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QFrame
6
5
  from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
7
6
 
8
7
 
@@ -59,7 +58,6 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
59
58
  self.current_view.setFocusPolicy(Qt.NoFocus)
60
59
  self.master_view.setFocusPolicy(Qt.NoFocus)
61
60
  self.current_brush_cursor = None
62
- self.setup_current_brush_cursor()
63
61
 
64
62
  def setup_layout(self):
65
63
  raise NotImplementedError("Subclasses must implement setup_layout")
@@ -180,10 +178,11 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
180
178
  self.master_view.setCursor(Qt.OpenHandCursor)
181
179
  self.current_view.setCursor(Qt.OpenHandCursor)
182
180
  else:
181
+ if self.brush_cursor is None or self.current_brush_cursor is None:
182
+ self.setup_brush_cursor()
183
183
  self.master_view.setCursor(Qt.BlankCursor)
184
184
  self.current_view.setCursor(Qt.BlankCursor)
185
- if self.brush_cursor:
186
- self.brush_cursor.show()
185
+ self.brush_cursor.show()
187
186
  super().enterEvent(event)
188
187
 
189
188
  def leaveEvent(self, event):
@@ -192,10 +191,10 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
192
191
  self.master_view.setCursor(Qt.ArrowCursor)
193
192
  self.current_view.setCursor(Qt.ArrowCursor)
194
193
  else:
195
- if self.brush_cursor is not None:
196
- self.brush_cursor.hide()
197
- if self.current_brush_cursor is not None:
198
- self.current_brush_cursor.hide()
194
+ if self.brush_cursor is None or self.current_brush_cursor is None:
195
+ self.setup_brush_cursor()
196
+ self.brush_cursor.hide()
197
+ self.current_brush_cursor.hide()
199
198
  self.master_view.setCursor(Qt.ArrowCursor)
200
199
  self.current_view.setCursor(Qt.ArrowCursor)
201
200
  super().leaveEvent(event)
@@ -248,33 +247,12 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
248
247
  def setup_brush_cursor(self):
249
248
  super().setup_brush_cursor()
250
249
  self.setup_current_brush_cursor()
251
- self.update_cursor_pen_width()
252
250
 
253
251
  def setup_current_brush_cursor(self):
254
252
  if not self.brush:
255
253
  return
256
- for item in self.current_scene.items():
257
- if isinstance(item, QGraphicsEllipseItem) and item != self.brush_preview:
258
- self.current_scene.removeItem(item)
259
- pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
260
- pen = QPen(QColor(255, 0, 0), pen_width, Qt.DotLine)
261
- brush = Qt.NoBrush
262
- self.current_brush_cursor = self.current_scene.addEllipse(
263
- 0, 0, self.brush.size, self.brush.size, pen, brush)
264
- self.current_brush_cursor.setZValue(1000)
265
- self.current_brush_cursor.hide()
266
-
267
- def update_current_brush_cursor(self, scene_pos):
268
- if not self.current_brush_cursor or not self.isVisible():
269
- return
270
- size = self.brush.size
271
- radius = size / 2
272
- self.current_brush_cursor.setRect(
273
- scene_pos.x() - radius, scene_pos.y() - radius, size, size)
274
- if self.brush_cursor and self.brush_cursor.isVisible():
275
- self.current_brush_cursor.show()
276
- else:
277
- self.current_brush_cursor.hide()
254
+ self.current_brush_cursor = self.create_alt_circle(
255
+ self.get_current_scene(), line_style=Qt.SolidLine)
278
256
 
279
257
  def update_cursor_pen_width(self):
280
258
  pen_width = super().update_cursor_pen_width()
@@ -287,23 +265,29 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
287
265
  def update_brush_cursor(self):
288
266
  if self.empty():
289
267
  return
268
+ if self.brush_cursor is None or self.current_brush_cursor is None:
269
+ self.setup_brush_cursor()
290
270
  self.update_cursor_pen_width()
271
+ if self.space_pressed:
272
+ cursor_style = Qt.OpenHandCursor if not self.scrolling else Qt.ClosedHandCursor
273
+ self.master_view.setCursor(cursor_style)
274
+ self.current_view.setCursor(cursor_style)
275
+ self.brush_cursor.hide()
276
+ self.current_brush_cursor.hide()
277
+ return
278
+ self.master_view.setCursor(Qt.BlankCursor)
279
+ self.current_view.setCursor(Qt.BlankCursor)
291
280
  mouse_pos_global = QCursor.pos()
292
281
  mouse_pos_current = self.current_view.mapFromGlobal(mouse_pos_global)
293
282
  mouse_pos_master = self.master_view.mapFromGlobal(mouse_pos_global)
294
283
  current_has_mouse = self.current_view.rect().contains(mouse_pos_current)
295
284
  master_has_mouse = self.master_view.rect().contains(mouse_pos_master)
285
+ self.current_brush_cursor.hide()
296
286
  if master_has_mouse:
297
- self.brush_preview.show()
287
+ if self.cursor_style == 'preview':
288
+ self.brush_preview.show()
298
289
  super().update_brush_cursor()
299
290
  self.sync_current_cursor_with_master()
300
- if self.space_pressed:
301
- cursor_style = Qt.OpenHandCursor if not self.scrolling else Qt.ClosedHandCursor
302
- self.master_view.setCursor(cursor_style)
303
- self.current_view.setCursor(cursor_style)
304
- else:
305
- self.master_view.setCursor(Qt.BlankCursor)
306
- self.current_view.setCursor(Qt.BlankCursor)
307
291
  elif current_has_mouse:
308
292
  self.brush_preview.hide()
309
293
  scene_pos = self.current_view.mapToScene(mouse_pos_current)
@@ -312,23 +296,12 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
312
296
  self.current_brush_cursor.setRect(
313
297
  scene_pos.x() - radius, scene_pos.y() - radius, size, size)
314
298
  self.current_brush_cursor.show()
315
- if self.brush_cursor:
316
- self.brush_cursor.setRect(
317
- scene_pos.x() - radius, scene_pos.y() - radius, size, size)
318
- self.brush_cursor.show()
319
- if self.space_pressed:
320
- cursor_style = Qt.OpenHandCursor \
321
- if not self.panning_current else Qt.ClosedHandCursor
322
- self.current_view.setCursor(cursor_style)
323
- self.master_view.setCursor(cursor_style)
324
- else:
325
- self.current_view.setCursor(Qt.BlankCursor)
326
- self.master_view.setCursor(Qt.BlankCursor)
299
+ self.brush_cursor.setRect(
300
+ scene_pos.x() - radius, scene_pos.y() - radius, size, size)
301
+ self.brush_cursor.show()
327
302
  else:
328
- if self.brush_cursor:
329
- self.brush_cursor.hide()
330
- if self.current_brush_cursor:
331
- self.current_brush_cursor.hide()
303
+ self.brush_cursor.hide()
304
+ self.current_brush_cursor.hide()
332
305
  self.master_view.setCursor(Qt.ArrowCursor)
333
306
  self.current_view.setCursor(Qt.ArrowCursor)
334
307
 
@@ -379,12 +352,14 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
379
352
  self.pixmap_item_master.setPixmap(pixmap)
380
353
  img_width, img_height = pixmap.width(), pixmap.height()
381
354
  self.set_max_min_scales(img_width, img_height)
382
- self.set_zoom_factor(1.0)
383
- self.master_view.fitInView(self.pixmap_item_master, Qt.KeepAspectRatio)
384
- self.set_zoom_factor(self.get_current_scale())
385
- self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), self.zoom_factor())))
355
+ view_rect = self.master_view.viewport().rect()
356
+ scale_x = view_rect.width() / img_width
357
+ scale_y = view_rect.height() / img_height
358
+ scale_factor = min(scale_x, scale_y)
359
+ scale_factor = max(self.min_scale(), min(scale_factor, self.max_scale()))
360
+ self.set_zoom_factor(scale_factor)
386
361
  self.master_view.resetTransform()
387
- self.master_scene.scale(self.zoom_factor(), self.zoom_factor())
362
+ self.master_view.scale(scale_factor, scale_factor)
388
363
  self.master_view.centerOn(self.pixmap_item_master)
389
364
  center = self.master_scene.sceneRect().center()
390
365
  self.brush_preview.setPos(max(0, min(center.x(), img_width)),
@@ -399,10 +374,9 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
399
374
  self.current_scene.setSceneRect(QRectF(pixmap.rect()))
400
375
  self.pixmap_item_current.setPixmap(pixmap)
401
376
  self.current_view.resetTransform()
402
- self.current_scene.scale(self.zoom_factor(), self.zoom_factor())
377
+ self.master_view.scale(self.zoom_factor(), self.zoom_factor())
403
378
  self.current_scene.setSceneRect(QRectF(self.pixmap_item_current.boundingRect()))
404
379
  self.center_image(self.current_view)
405
- self.update_cursor_pen_width()
406
380
 
407
381
  def arrange_images(self):
408
382
  if self.status.empty():
@@ -415,13 +389,6 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
415
389
  self.center_image(self.master_view)
416
390
  self.apply_zoom()
417
391
 
418
- def set_brush(self, brush):
419
- super().set_brush(brush)
420
- if self.brush_cursor:
421
- self.master_scene.removeItem(self.brush_cursor)
422
- self.setup_brush_cursor()
423
- self.setup_current_brush_cursor()
424
-
425
392
  def clear_image(self):
426
393
  super().clear_image()
427
394
  self.setCursor(Qt.ArrowCursor)
@@ -439,10 +406,8 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
439
406
  size = self.brush.size
440
407
  radius = size / 2
441
408
  self.current_brush_cursor.setRect(
442
- scene_pos.x() - radius,
443
- scene_pos.y() - radius,
444
- size, size
445
- )
409
+ scene_pos.x() - radius, scene_pos.y() - radius,
410
+ size, size)
446
411
  if self.brush_cursor.isVisible():
447
412
  self.current_brush_cursor.show()
448
413
  else:
@@ -1,17 +1,79 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0903, R0902, E1101, R0914
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0903, R0902, E1101, R0914, R0913, R0917
2
2
  import math
3
3
  from abc import abstractmethod
4
4
  import numpy as np
5
5
  from PySide6.QtCore import Qt, QPointF, QTime, QPoint, Signal, QRectF
6
- from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor, QPixmap
6
+ from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor, QPixmap, QPainterPath
7
7
  from PySide6.QtWidgets import (
8
- QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem)
8
+ QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
9
+ QGraphicsItemGroup, QGraphicsPathItem)
9
10
  from .. config.gui_constants import gui_constants
10
11
  from .layer_collection import LayerCollectionHandler
11
12
  from .brush_gradient import create_default_brush_gradient
12
13
  from .brush_preview import BrushPreviewItem
13
14
 
14
15
 
16
+ class BrushCursor(QGraphicsItemGroup):
17
+ def __init__(self, x0, y0, size, pen, brush):
18
+ super().__init__()
19
+ self._pen = pen
20
+ self._radius = size / 2
21
+ self._brush = brush
22
+ self._rect = QRectF(x0 - self._radius, y0 - self._radius, size, size)
23
+ self._arc_items = []
24
+ self._create_arcs()
25
+
26
+ def _point_on_circle(self, phi_deg):
27
+ phi = phi_deg / 180.0 * math.pi
28
+ x0 = self._rect.x() + self._radius
29
+ y0 = self._rect.y() + self._radius
30
+ return x0 + self._radius * math.cos(phi), y0 - self._radius * math.sin(phi)
31
+
32
+ def _create_arcs(self):
33
+ for item in self._arc_items:
34
+ self.removeFromGroup(item)
35
+ if item.scene():
36
+ item.scene().removeItem(item)
37
+ self._arc_items = []
38
+ half_gap = 20
39
+ arcs = [half_gap, 90 + half_gap, 180 + half_gap, 270 + half_gap]
40
+ span_angle = 90 - 2 * half_gap
41
+ for start_angle in arcs:
42
+ path = QPainterPath()
43
+ path.moveTo(*self._point_on_circle(start_angle))
44
+ path.arcTo(self._rect, start_angle, span_angle)
45
+ arc_item = QGraphicsPathItem(path)
46
+ arc_item.setPen(self._pen)
47
+ arc_item.setBrush(Qt.NoBrush)
48
+ self.addToGroup(arc_item)
49
+ self._arc_items.append(arc_item)
50
+
51
+ # pylint: disable=C0103
52
+ def setPen(self, pen):
53
+ self._pen = pen
54
+ for item in self._arc_items:
55
+ item.setPen(pen)
56
+
57
+ def pen(self):
58
+ return self._pen
59
+
60
+ def setBrush(self, brush):
61
+ self._brush = brush
62
+ for item in self._arc_items:
63
+ item.setBrush(Qt.NoBrush)
64
+
65
+ def brush(self):
66
+ return self._brush
67
+
68
+ def setRect(self, x, y, w, h):
69
+ self._rect = QRectF(x, y, w, h)
70
+ self._create_arcs()
71
+
72
+ def rect(self):
73
+ return self._rect
74
+ # pylint: enable=C0103
75
+
76
+
15
77
  class ViewSignals:
16
78
  temp_view_requested = Signal(bool)
17
79
  brush_operation_started = Signal(QPoint)
@@ -141,12 +203,12 @@ class ViewStrategy(LayerCollectionHandler):
141
203
  self.arrange_images()
142
204
 
143
205
  def update_cursor_pen_width(self):
144
- pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
206
+ width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
145
207
  if self.brush_cursor is not None:
146
- master_pen = self.brush_cursor.pen()
147
- master_pen.setWidthF(pen_width)
148
- self.brush_cursor.setPen(master_pen)
149
- return pen_width
208
+ pen = self.brush_cursor.pen()
209
+ pen.setWidthF(width)
210
+ self.brush_cursor.setPen(pen)
211
+ return width
150
212
 
151
213
  def set_allow_cursor_preview(self, state):
152
214
  self.allow_cursor_preview = state
@@ -217,6 +279,9 @@ class ViewStrategy(LayerCollectionHandler):
217
279
 
218
280
  def set_master_image_np(self, img):
219
281
  self.set_master_image(self.numpy_to_qimage(img))
282
+ if self.brush_cursor is None:
283
+ self.setup_brush_cursor()
284
+ self.show_master()
220
285
 
221
286
  def numpy_to_qimage(self, array):
222
287
  if array is None:
@@ -319,33 +384,51 @@ class ViewStrategy(LayerCollectionHandler):
319
384
  self.update_brush_cursor()
320
385
  self.update_cursor_pen_width()
321
386
 
322
- def setup_outline_style(self):
323
- self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
324
- gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
325
- self.brush_cursor.setBrush(Qt.NoBrush)
326
-
327
387
  def setup_simple_brush_style(self, center_x, center_y, radius):
328
388
  gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
329
389
  self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
330
390
  gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
331
391
  self.brush_cursor.setBrush(QBrush(gradient))
332
392
 
333
- def setup_brush_cursor(self):
334
- if not self.brush:
335
- return
336
- scene = self.get_master_scene()
393
+ def create_circle(self, scene, line_style=Qt.SolidLine):
337
394
  for item in scene.items():
338
395
  if isinstance(item, QGraphicsEllipseItem) and item != self.brush_preview:
339
396
  scene.removeItem(item)
340
- pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
397
+ pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
398
+ pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), pen_width, line_style)
399
+ brush = Qt.NoBrush
400
+ scene_center = scene.sceneRect().center()
401
+ brush_cursor = scene.addEllipse(
402
+ scene_center.x(), scene_center.y(),
403
+ self.brush.size, self.brush.size, pen, brush)
404
+ brush_cursor.setZValue(1000)
405
+ brush_cursor.hide()
406
+ return brush_cursor
407
+
408
+ def create_alt_circle(self, scene, line_style=Qt.SolidLine):
409
+ for item in scene.items():
410
+ if isinstance(item, BrushCursor) and item != self.brush_preview:
411
+ scene.removeItem(item)
412
+ pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
413
+ pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), pen_width, line_style)
341
414
  brush = Qt.NoBrush
342
- self.brush_cursor = scene.addEllipse(
343
- 0, 0, self.brush.size, self.brush.size, pen, brush)
344
- self.brush_cursor.setZValue(1000)
345
- self.brush_cursor.hide()
415
+ scene_center = scene.sceneRect().center()
416
+ brush_cursor = BrushCursor(
417
+ scene_center.x(), scene_center.y(),
418
+ self.brush.size, pen, brush
419
+ )
420
+ brush_cursor.setZValue(1000)
421
+ brush_cursor.hide()
422
+ scene.addItem(brush_cursor)
423
+ return brush_cursor
424
+
425
+ def setup_brush_cursor(self):
426
+ if not self.brush:
427
+ return
428
+ self.brush_cursor = self.create_circle(self.get_master_scene())
346
429
 
347
430
  def update_brush_cursor(self):
348
- if self.empty() or not self.brush_cursor or not self.isVisible():
431
+ if self.empty() or self.brush_cursor is None or not self.isVisible():
349
432
  return
350
433
  self.update_cursor_pen_width()
351
434
  master_view = self.get_master_view()
@@ -359,7 +442,6 @@ class ViewStrategy(LayerCollectionHandler):
359
442
  self.brush_cursor.setRect(scene_pos.x() - radius, scene_pos.y() - radius, size, size)
360
443
  allow_cursor_preview = self.display_manager.allow_cursor_preview()
361
444
  if self.cursor_style == 'preview':
362
- self.setup_outline_style()
363
445
  if allow_cursor_preview:
364
446
  self.brush_cursor.hide()
365
447
  pos = QCursor.pos()
@@ -371,9 +453,7 @@ class ViewStrategy(LayerCollectionHandler):
371
453
  self.brush_preview.update(scene_pos, int(size))
372
454
  else:
373
455
  self.brush_preview.hide()
374
- if self.cursor_style == 'outline':
375
- self.setup_outline_style()
376
- else:
456
+ if self.cursor_style != 'outline':
377
457
  self.setup_simple_brush_style(scene_pos.x(), scene_pos.y(), radius)
378
458
  if not self.brush_cursor.isVisible():
379
459
  self.brush_cursor.show()
@@ -427,7 +507,6 @@ class ViewStrategy(LayerCollectionHandler):
427
507
  def keyReleaseEvent(self, event):
428
508
  if self.empty():
429
509
  return
430
- self.update_brush_cursor()
431
510
  if event.key() == Qt.Key_Space:
432
511
  self.space_pressed = False
433
512
  if not self.scrolling:
@@ -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
@@ -1,5 +1,5 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=XyInpe-Rlf1hgwkiRXtR0uPYm4ZwRFpgchjcpZZ9Pms,21
2
+ shinestacker/_version.py,sha256=oU1lLCdhmPP8CUfgzMFfhCPjCfk4eDfd8WSwOfXNBOk,21
3
3
  shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
4
4
  shinestacker/algorithms/align.py,sha256=mb44u-YxZI1TTSHz81nRpX_2c8awlOhnGrK0LyfTQeQ,33543
5
5
  shinestacker/algorithms/align_auto.py,sha256=pJetw6zZEWQLouzcelkI8gD4cPiOp887ePXzVbm0E6Q,3800
@@ -22,16 +22,17 @@ shinestacker/algorithms/vignetting.py,sha256=gJOv-FN3GnTgaVn70W_6d-qbw3WmqinDiO9
22
22
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
23
23
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
25
+ shinestacker/app/args.py,sha256=TlfMR5GBd6Zz9wZNrN7CGJ1_hm1FnLZn5EoP5mkRHrk,776
25
26
  shinestacker/app/gui_utils.py,sha256=fSpkwPXTON_l676UHdAnJNrGq7BPbSlPOiHpOF_LZaI,2519
26
27
  shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
27
- shinestacker/app/main.py,sha256=JRfobAg_stqk7FZbGWNQChfdQHRzMXqFzVXe1gOci_I,10459
28
+ shinestacker/app/main.py,sha256=dcitc5vwIIyIXeDfZwQnC7KHRCdd3FaVJWyaU8mK86c,10935
28
29
  shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
29
- shinestacker/app/project.py,sha256=oopOiqU6bOK1cQdpot88z49KbKrlB-LAz_q4-8Iui0U,2819
30
- shinestacker/app/retouch.py,sha256=dpSozNWSxL6wIO0SMjoviDbXZbbfRN_rVLjeL324c54,2527
30
+ shinestacker/app/project.py,sha256=X98pK_mMtE_NefTUZfebEaP1YCsVY97hcQD4bSxuNyY,2777
31
+ shinestacker/app/retouch.py,sha256=wlk-tHaei5YAFinGZWyzBopUhUqyxMT6jSH-4DMEwo8,2659
31
32
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
32
33
  shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
33
34
  shinestacker/config/constants.py,sha256=EEdr7pZg4JpbIjUWaP7kJQfTuBB85FN739myDNAfn8A,8301
34
- shinestacker/config/gui_constants.py,sha256=YigJ5pRKjzjQKAN-ulosS6zLfnTj2BQESvVIvqXNzjA,2786
35
+ shinestacker/config/gui_constants.py,sha256=Aqan-AdUjtlARXpsefQvWW2uKVv1tvwc0gfKyo7xud4,2787
35
36
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
36
37
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
37
38
  shinestacker/core/core_utils.py,sha256=1LYj19Dfc9jZN9-4dlf1paximDH5WZYa7DXvKr7R7QY,1719
@@ -50,7 +51,7 @@ shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPu
50
51
  shinestacker/gui/gui_run.py,sha256=zr7x4BVmM0n_ZRsSEaJVVKvHSWHuwhftgkUvgeg90gU,15767
51
52
  shinestacker/gui/main_window.py,sha256=5k_9TiZT9idKCmovUFYpUTSEQQj-DMQrlyq9dAgY1MU,24800
52
53
  shinestacker/gui/menu_manager.py,sha256=b5Cxh6uddOlio8i7fRISbGDJI-oe0ds6LIF5dWM7leI,11263
53
- shinestacker/gui/new_project.py,sha256=VSUaq1xm9CR0gimKHRKfCdQOQ-ErE1sxGmu6x14nlAQ,16113
54
+ shinestacker/gui/new_project.py,sha256=z8e3EhRMB-KtoPwYQSiKLSOQ2dS0-Okm7zVw21B7zy8,16391
54
55
  shinestacker/gui/project_controller.py,sha256=W4sbBGEPVtfF9F1rC-6Y0oKLq_y94HuFBvZRj87xNKQ,16272
55
56
  shinestacker/gui/project_converter.py,sha256=Gmna0HwbvACcXiX74TaQYumif8ZV8sZ2APLTMM-L1mU,7436
56
57
  shinestacker/gui/project_editor.py,sha256=lSgQ42IoaobHs-NQQWT88Qhg5l7nu5ejxAO5VgIupr8,25498
@@ -73,7 +74,7 @@ shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
73
74
  shinestacker/retouch/base_filter.py,sha256=gvRGvhOhhWS05jKOXyNdMnia3b7rscQqR8-xnySIWFc,10260
74
75
  shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
75
76
  shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
76
- shinestacker/retouch/brush_preview.py,sha256=03x7Hh-5g4JEQgBCK5mGUtVDtiOnJPOdU1JgeUJhGaA,4945
77
+ shinestacker/retouch/brush_preview.py,sha256=cOFVMCbEsgR_alzmr_-LLghtGU_unrE-hAjLHcvrZAY,5484
77
78
  shinestacker/retouch/brush_tool.py,sha256=8uVncTA375uC3Nhp2YM0eZjpOR-nN47i2eGjN8tJzOU,8714
78
79
  shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
79
80
  shinestacker/retouch/display_manager.py,sha256=wwbX4n7ulg53fM3E5QL5YEb5IFV0jN8za6yfYc74anA,8624
@@ -81,24 +82,24 @@ shinestacker/retouch/exif_data.py,sha256=LF-fRXW-reMq-xJ_QRE5j8DC2LVGKIlC6MR3QbC
81
82
  shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
82
83
  shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
83
84
  shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
84
- shinestacker/retouch/image_editor_ui.py,sha256=0EW-jnKrQz9Z_xLS51AltWul7lHst4fAvubiWNH-g2o,33456
85
+ shinestacker/retouch/image_editor_ui.py,sha256=ifrlHafXdELwCXoVfUiSM7jcz3O6qH7z2WjNpNPEmUs,33505
85
86
  shinestacker/retouch/image_view_status.py,sha256=bdIhsXiYXm7eyjkTGWkw5PRShzaF_by-g7daqgmhwjM,1858
86
- shinestacker/retouch/image_viewer.py,sha256=jkFVuBxWxoZqtaOS8N1R8iZ9WnyGfKGdczEdET2KTWU,4452
87
- shinestacker/retouch/io_gui_handler.py,sha256=Upt8VWwDUrB-UnqCI54SQQwfR4w9kfznCdRL2UwupWQ,12010
87
+ shinestacker/retouch/image_viewer.py,sha256=D7feFlaZkR2nzJ9VcKcVg8lYPS7AMjA_uxJs9L099kQ,4412
88
+ shinestacker/retouch/io_gui_handler.py,sha256=BRQ5eSt1tCMDYtOqxfdYGhx2BLCrncfNrNLGuWIy5Rk,11873
88
89
  shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
89
90
  shinestacker/retouch/layer_collection.py,sha256=fZlGrkm9-Ycc7AOzFSpImhafiTieBeCZRk-UlvlFHbo,5819
90
- shinestacker/retouch/overlaid_view.py,sha256=NoFEwokTqbx41xnzYEHS0xhprHEAd2pRlH5NRAHxc2Y,7960
91
- shinestacker/retouch/shortcuts_help.py,sha256=EDxwR7MZwUC9NHLjvqAlh5iEHT9g3g8Tzl18VUGErI4,4130
92
- shinestacker/retouch/sidebyside_view.py,sha256=d9Pc2P_apTd0krsERIuEVc8RYBxiXs5tC7X9__t8TBc,18883
91
+ shinestacker/retouch/overlaid_view.py,sha256=0V6Y0wVmf7vPzhB4O9BSF8UwBnw0RdBdYCt_tHD4vls,8478
92
+ shinestacker/retouch/shortcuts_help.py,sha256=BFWTT5QvodqMhqa_9LI25hZqjICfckgyWG4fGrGzvnM,4283
93
+ shinestacker/retouch/sidebyside_view.py,sha256=We4hY_ZGwINPVPHACPGYFPaiU3Khpax7Sf9Xur317FA,17358
93
94
  shinestacker/retouch/transformation_manager.py,sha256=NSHGUF-JFv4Y81gSvizjQCTp49TLo1so7c0WoUElO08,1812
94
95
  shinestacker/retouch/undo_manager.py,sha256=cKUkqnJtnJ-Hq-LQs5Bv49FC6qkG6XSw9oCVySJ8jS0,4312
95
96
  shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
96
- shinestacker/retouch/view_strategy.py,sha256=ex_bNym9pBXk9GDcpZr1vh4sJXx4ytrzavLRkHzeEXc,21234
97
+ shinestacker/retouch/view_strategy.py,sha256=5AiSYH_FkeIhaUMtQwZ3v1_8r6KckvOYPFoufEtw12Y,23877
97
98
  shinestacker/retouch/vignetting_filter.py,sha256=MA97rQkSL0D-Nh-n2L4AiPR064RoTROkvza4tw84g9U,3658
98
99
  shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
99
- shinestacker-1.5.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
100
- shinestacker-1.5.0.dist-info/METADATA,sha256=FXzUo0Sa94vwqOVxvbUHFIeRrjRvGgNMqnmUgooYcIY,6978
101
- shinestacker-1.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
102
- shinestacker-1.5.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
103
- shinestacker-1.5.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
104
- shinestacker-1.5.0.dist-info/RECORD,,
100
+ shinestacker-1.5.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
101
+ shinestacker-1.5.1.dist-info/METADATA,sha256=FrhfVRsmH4sx7-cQxDajn-zZinLt-UIhhHf2xYZNzvM,6978
102
+ shinestacker-1.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
103
+ shinestacker-1.5.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
104
+ shinestacker-1.5.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
105
+ shinestacker-1.5.1.dist-info/RECORD,,