shinestacker 1.5.0__py3-none-any.whl → 1.5.2__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.2'
@@ -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,16 +1,25 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0718, R0915, R0903, R0913, R0917, R0902, R0914
2
2
  import traceback
3
- from abc import ABC, abstractmethod
3
+ from abc import abstractmethod
4
4
  import numpy as np
5
+ from PySide6.QtCore import Qt, QThread, QTimer, QObject, Signal
5
6
  from PySide6.QtWidgets import (
6
7
  QHBoxLayout, QLabel, QSlider, QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox)
7
- from PySide6.QtCore import Qt, Signal, QThread, QTimer
8
+ from .layer_collection import LayerCollectionHandler
8
9
 
9
10
 
10
- class BaseFilter(ABC):
11
- def __init__(self, name, editor, allow_partial_preview=True,
11
+ class BaseFilter(QObject, LayerCollectionHandler):
12
+ update_master_thumbnail_requested = Signal()
13
+ mark_as_modified_requested = Signal()
14
+ filter_gui_set_enabled_requested = Signal(bool)
15
+
16
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
17
+ allow_partial_preview=True,
12
18
  partial_preview_threshold=0.75, preview_at_startup=False):
13
- self.editor = editor
19
+ QObject.__init__(self, parent)
20
+ LayerCollectionHandler.__init__(self, layer_collection)
21
+ self.image_viewer = image_viewer
22
+ self.undo_manager = undo_manager
14
23
  self.name = name
15
24
  self.allow_partial_preview = allow_partial_preview
16
25
  self.partial_preview_threshold = partial_preview_threshold
@@ -31,11 +40,16 @@ class BaseFilter(ABC):
31
40
  def apply(self, image, *params):
32
41
  pass
33
42
 
43
+ def connect_signals(self, update_master_thumbnail, mark_as_modified, filter_gui_set_enabled):
44
+ self.update_master_thumbnail_requested.connect(update_master_thumbnail)
45
+ self.mark_as_modified_requested.connect(mark_as_modified)
46
+ self.filter_gui_set_enabled_requested.connect(filter_gui_set_enabled)
47
+
34
48
  def run_with_preview(self, **kwargs):
35
- if self.editor.has_no_master_layer():
49
+ if self.has_no_master_layer():
36
50
  return
37
- self.editor.copy_master_layer()
38
- dlg = QDialog(self.editor)
51
+ self.copy_master_layer()
52
+ dlg = QDialog(self.parent())
39
53
  layout = QVBoxLayout(dlg)
40
54
  active_worker = None
41
55
  last_request_id = 0
@@ -46,8 +60,8 @@ class BaseFilter(ABC):
46
60
  def cleanup():
47
61
  nonlocal active_worker, dialog_closed # noqa
48
62
  dialog_closed = True
49
- self.editor.restore_master_layer()
50
- self.editor.image_viewer.update_master_display()
63
+ self.restore_master_layer()
64
+ self.image_viewer.update_master_display()
51
65
  if active_worker and active_worker.isRunning():
52
66
  active_worker.wait()
53
67
  initial_timer.stop()
@@ -58,13 +72,13 @@ class BaseFilter(ABC):
58
72
  if dialog_closed or request_id != expected_id:
59
73
  return
60
74
  if region:
61
- current_region = self.editor.image_viewer.get_visible_image_portion()[1]
75
+ current_region = self.image_viewer.get_visible_image_portion()[1]
62
76
  if current_region == region:
63
- self.editor.set_master_layer(img)
64
- self.editor.image_viewer.update_master_display()
77
+ self.set_master_layer(img)
78
+ self.image_viewer.update_master_display()
65
79
  else:
66
- self.editor.set_master_layer(img)
67
- self.editor.image_viewer.update_master_display()
80
+ self.set_master_layer(img)
81
+ self.image_viewer.update_master_display()
68
82
  try:
69
83
  dlg.activateWindow()
70
84
  except Exception:
@@ -84,10 +98,10 @@ class BaseFilter(ABC):
84
98
  current_id = last_request_id
85
99
  visible_region = None
86
100
  if kwargs.get('partial_preview', self.allow_partial_preview):
87
- visible_data = self.editor.image_viewer.get_visible_image_portion()
101
+ visible_data = self.image_viewer.get_visible_image_portion()
88
102
  if visible_data:
89
103
  visible_img, visible_region = visible_data
90
- master_img = self.editor.master_layer_copy()
104
+ master_img = self.master_layer_copy()
91
105
  if visible_img.size < master_img.size * self.partial_preview_threshold:
92
106
  params = tuple(self.get_params() or ())
93
107
  worker = self.PreviewWorker(
@@ -107,14 +121,14 @@ class BaseFilter(ABC):
107
121
  params = tuple(self.get_params() or ())
108
122
  worker = self.PreviewWorker(
109
123
  self.apply,
110
- args=(self.editor.master_layer_copy(), *params),
124
+ args=(self.master_layer_copy(), *params),
111
125
  request_id=current_id
112
126
  )
113
127
  else:
114
128
  params = tuple(self.get_params() or ())
115
129
  worker = self.PreviewWorker(
116
130
  self.apply,
117
- args=(self.editor.master_layer_copy(), *params),
131
+ args=(self.master_layer_copy(), *params),
118
132
  request_id=current_id
119
133
  )
120
134
  active_worker = worker
@@ -123,8 +137,8 @@ class BaseFilter(ABC):
123
137
  active_worker.start()
124
138
 
125
139
  def restore_original():
126
- self.editor.restore_master_layer()
127
- self.editor.image_viewer.update_master_display()
140
+ self.restore_master_layer()
141
+ self.image_viewer.update_master_display()
128
142
  try:
129
143
  dlg.activateWindow()
130
144
  except Exception:
@@ -139,26 +153,34 @@ class BaseFilter(ABC):
139
153
  if accepted:
140
154
  params = tuple(self.get_params() or ())
141
155
  try:
142
- h, w = self.editor.master_layer().shape[:2]
156
+ h, w = self.master_layer().shape[:2]
143
157
  except Exception:
144
- h, w = self.editor.master_layer_copy().shape[:2]
158
+ h, w = self.master_layer_copy().shape[:2]
145
159
  try:
146
- self.editor.undo_manager.extend_undo_area(0, 0, w, h)
147
- self.editor.undo_manager.save_undo_state(
148
- self.editor.master_layer_copy(),
160
+ self.undo_manager.extend_undo_area(0, 0, w, h)
161
+ self.undo_manager.save_undo_state(
162
+ self.master_layer_copy(),
149
163
  self.name
150
164
  )
151
165
  except Exception:
152
166
  pass
153
- final_img = self.apply(self.editor.master_layer_copy(), *params)
154
- self.editor.set_master_layer(final_img)
155
- self.editor.copy_master_layer()
156
- self.editor.image_viewer.update_master_display()
157
- self.editor.display_manager.update_master_thumbnail()
158
- self.editor.mark_as_modified()
167
+ final_img = self.apply(self.master_layer_copy(), *params)
168
+ self.set_master_layer(final_img)
169
+ self.copy_master_layer()
170
+ self.image_viewer.update_master_display()
171
+ self.update_master_thumbnail_requested.emit()
172
+ self.mark_as_modified_requested.emit()
159
173
  else:
160
174
  restore_original()
161
175
 
176
+ def connect_preview_toggle(self, preview_check, do_preview, restore_original):
177
+ def on_toggled(checked):
178
+ if checked:
179
+ do_preview()
180
+ else:
181
+ restore_original()
182
+ preview_check.toggled.connect(on_toggled)
183
+
162
184
  def create_base_widgets(self, layout, buttons, preview_latency, parent):
163
185
  self.preview_check = QCheckBox("Preview")
164
186
  self.preview_check.setChecked(self.preview_at_startup)
@@ -199,10 +221,12 @@ class BaseFilter(ABC):
199
221
 
200
222
 
201
223
  class OneSliderBaseFilter(BaseFilter):
202
- def __init__(self, name, editor, max_value, initial_value, title,
224
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
225
+ max_value, initial_value, title,
203
226
  allow_partial_preview=True, partial_preview_threshold=0.5,
204
227
  preview_at_startup=True):
205
- super().__init__(name, editor, allow_partial_preview,
228
+ super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
229
+ allow_partial_preview,
206
230
  partial_preview_threshold, preview_at_startup)
207
231
  self.max_range = 500
208
232
  self.max_value = max_value
@@ -234,7 +258,7 @@ class OneSliderBaseFilter(BaseFilter):
234
258
  self.preview_timer.timeout.connect(do_preview)
235
259
 
236
260
  slider_local.valueChanged.connect(self.config_changed)
237
- self.editor.connect_preview_toggle(
261
+ self.connect_preview_toggle(
238
262
  self.preview_check, self.do_preview_delayed, restore_original)
239
263
  self.button_box.accepted.connect(dlg.accept)
240
264
  self.button_box.rejected.connect(dlg.reject)
@@ -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()
@@ -1,11 +1,12 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, W0221
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0917
2
2
  from .base_filter import OneSliderBaseFilter
3
3
  from .. algorithms.denoise import denoise
4
4
 
5
5
 
6
6
  class DenoiseFilter(OneSliderBaseFilter):
7
- def __init__(self, name, editor):
8
- super().__init__(name, editor, 10.0, 2.5, "Denoise",
7
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
8
+ super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
9
+ 10.0, 2.5, "Denoise",
9
10
  allow_partial_preview=True, preview_at_startup=False)
10
11
 
11
12
  def apply(self, image, strength):
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902, R0914
2
2
  import numpy as np
3
3
  from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
4
4
  QAbstractItemView)
@@ -38,7 +38,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
38
38
  self.update_timer = QTimer()
39
39
  self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
40
40
  self.update_timer.timeout.connect(self.process_pending_updates)
41
- self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
42
41
 
43
42
  def process_pending_updates(self):
44
43
  if self.needs_update:
@@ -110,14 +109,15 @@ class DisplayManager(QObject, LayerCollectionHandler):
110
109
  self.thumbnail_list, "Rename Label", "New label name:", text=old_label)
111
110
  if ok and new_label and new_label != old_label:
112
111
  label_widget.setText(new_label)
113
- self.set_layer_labels(i, new_label)
112
+ self.set_layer_label(i, new_label)
113
+ self.status_message_requested.emit("Label renamed.")
114
114
 
115
115
  label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
116
116
  content_layout.addWidget(label_widget)
117
117
  container_layout.addWidget(content_widget)
118
118
  if is_current:
119
119
  container.setStyleSheet(
120
- f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
120
+ f"#thumbnailContainer{{ border: 2px solid {gui_constants.THUMB_HI_COLOR}; }}")
121
121
  else:
122
122
  container.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
123
123
  item = QListWidgetItem()
@@ -128,7 +128,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
128
128
  if is_current:
129
129
  self.thumbnail_list.setCurrentItem(item)
130
130
 
131
- def highlight_thumbnail(self, index):
131
+ def highlight_thumbnail(self, index, color=gui_constants.THUMB_HI_COLOR):
132
132
  for i in range(self.thumbnail_list.count()):
133
133
  item = self.thumbnail_list.item(i)
134
134
  widget = self.thumbnail_list.itemWidget(item)
@@ -139,7 +139,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
139
139
  widget = self.thumbnail_list.itemWidget(current_item)
140
140
  if widget:
141
141
  widget.setStyleSheet(
142
- f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
142
+ f"#thumbnailContainer{{ border: 2px solid {color}; }}")
143
143
  self.thumbnail_list.setCurrentRow(index)
144
144
  self.thumbnail_list.scrollToItem(
145
145
  self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
@@ -147,28 +147,26 @@ class DisplayManager(QObject, LayerCollectionHandler):
147
147
  def _master_refresh_and_thumb(self):
148
148
  self.image_viewer.show_master()
149
149
  self.refresh_master_view()
150
- self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
151
- self.highlight_thumbnail(self.current_layer_idx())
150
+ self.highlight_thumbnail(self.current_layer_idx(), gui_constants.THUMB_LO_COLOR)
152
151
 
153
152
  def _current_refresh_and_thumb(self):
154
153
  self.image_viewer.show_current()
155
154
  self.refresh_current_view()
156
- self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
157
- self.highlight_thumbnail(self.current_layer_idx())
155
+ self.highlight_thumbnail(self.current_layer_idx(), gui_constants.THUMB_HI_COLOR)
158
156
 
159
157
  def set_view_master(self):
160
158
  if self.has_no_master_layer():
161
159
  return
162
160
  self.view_mode = 'master'
163
161
  self._master_refresh_and_thumb()
164
- self.status_message_requested.emit("View mode: Master")
162
+ self.status_message_requested.emit("View: Master.")
165
163
 
166
164
  def set_view_individual(self):
167
165
  if self.has_no_master_layer():
168
166
  return
169
167
  self.view_mode = 'individual'
170
168
  self._current_refresh_and_thumb()
171
- self.status_message_requested.emit("View mode: Individual layers")
169
+ self.status_message_requested.emit("View: Individual layers.")
172
170
 
173
171
  def refresh_master_view(self):
174
172
  if self.has_no_master_layer():
@@ -186,19 +184,16 @@ class DisplayManager(QObject, LayerCollectionHandler):
186
184
  def start_temp_view(self):
187
185
  if self.view_mode == 'master':
188
186
  self._current_refresh_and_thumb()
189
- self.status_message_requested.emit("Temporary view: Individual layer")
187
+ self.status_message_requested.emit("Temporary view: Individual layer.")
190
188
  else:
191
189
  self._master_refresh_and_thumb()
192
190
  self.image_viewer.strategy.brush_preview.hide()
193
- self.status_message_requested.emit("Temporary view: Master")
191
+ self.status_message_requested.emit("Temporary view: Master.")
194
192
 
195
193
  def end_temp_view(self):
196
194
  if self.view_mode == 'master':
197
195
  self._master_refresh_and_thumb()
198
- self.status_message_requested.emit("View mode: Master")
196
+ self.status_message_requested.emit("View mode: Master.")
199
197
  else:
200
198
  self._current_refresh_and_thumb()
201
- self.status_message_requested.emit("View: Individual layer")
202
-
203
- def allow_cursor_preview(self):
204
- return self.view_mode == 'master'
199
+ self.status_message_requested.emit("View: Individual layer.")
@@ -1,11 +1,19 @@
1
- # pylint: disable=C0114, C0115, C0116
1
+ # pylint: disable=C0114, C0115, C0116, R0913, R0917
2
2
  class FilterManager:
3
3
  def __init__(self, editor):
4
4
  self.editor = editor
5
+ self.image_viewer = editor.image_viewer
6
+ self.layer_collection = editor.layer_collection
7
+ self.undo_manager = editor.undo_manager
5
8
  self.filters = {}
6
9
 
7
- def register_filter(self, name, filter_class):
8
- self.filters[name] = filter_class(name, self.editor)
10
+ def register_filter(self, name, filter_class,
11
+ update_master_thumbnail, mark_as_modified, filter_gui_set_enabled):
12
+ filter_obj = filter_class(
13
+ name, self.editor, self.image_viewer, self.layer_collection, self.undo_manager)
14
+ self.filters[name] = filter_obj
15
+ filter_obj.connect_signals(
16
+ update_master_thumbnail, mark_as_modified, filter_gui_set_enabled)
9
17
 
10
18
  def apply(self, name, **kwargs):
11
19
  if name in self.filters: