shinestacker 1.5.4__py3-none-any.whl → 1.6.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.

Files changed (45) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/multilayer.py +1 -1
  3. shinestacker/algorithms/stack.py +17 -9
  4. shinestacker/app/args_parser_opts.py +4 -0
  5. shinestacker/app/gui_utils.py +10 -2
  6. shinestacker/app/main.py +8 -3
  7. shinestacker/app/project.py +7 -3
  8. shinestacker/app/retouch.py +8 -1
  9. shinestacker/app/settings_dialog.py +171 -0
  10. shinestacker/config/app_config.py +30 -0
  11. shinestacker/config/constants.py +3 -0
  12. shinestacker/config/gui_constants.py +4 -2
  13. shinestacker/config/settings.py +110 -0
  14. shinestacker/core/core_utils.py +3 -12
  15. shinestacker/core/logging.py +3 -2
  16. shinestacker/gui/action_config.py +6 -5
  17. shinestacker/gui/action_config_dialog.py +17 -74
  18. shinestacker/gui/config_dialog.py +78 -0
  19. shinestacker/gui/main_window.py +6 -6
  20. shinestacker/gui/menu_manager.py +2 -0
  21. shinestacker/gui/new_project.py +2 -1
  22. shinestacker/gui/project_controller.py +8 -6
  23. shinestacker/gui/project_model.py +16 -1
  24. shinestacker/gui/recent_file_manager.py +3 -21
  25. shinestacker/retouch/base_filter.py +1 -1
  26. shinestacker/retouch/display_manager.py +48 -7
  27. shinestacker/retouch/image_editor_ui.py +27 -36
  28. shinestacker/retouch/image_view_status.py +4 -1
  29. shinestacker/retouch/image_viewer.py +17 -9
  30. shinestacker/retouch/io_gui_handler.py +96 -44
  31. shinestacker/retouch/io_threads.py +78 -0
  32. shinestacker/retouch/layer_collection.py +12 -0
  33. shinestacker/retouch/overlaid_view.py +13 -5
  34. shinestacker/retouch/paint_area_manager.py +30 -0
  35. shinestacker/retouch/sidebyside_view.py +32 -16
  36. shinestacker/retouch/transformation_manager.py +1 -3
  37. shinestacker/retouch/undo_manager.py +15 -13
  38. shinestacker/retouch/view_strategy.py +79 -26
  39. {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/METADATA +1 -1
  40. {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/RECORD +44 -39
  41. shinestacker/retouch/io_manager.py +0 -69
  42. {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/WHEEL +0 -0
  43. {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/entry_points.txt +0 -0
  44. {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/licenses/LICENSE +0 -0
  45. {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,11 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902
2
- from PySide6.QtCore import QObject, QRectF
2
+ from PySide6.QtCore import QObject, QRectF, Signal
3
3
  from PySide6.QtGui import QPixmap
4
4
 
5
5
 
6
6
  class ImageViewStatus(QObject):
7
+ set_zoom_factor_requested = Signal(float)
8
+
7
9
  def __init__(self, parent=None):
8
10
  super().__init__(parent)
9
11
  self.pixmap_master = QPixmap()
@@ -53,6 +55,7 @@ class ImageViewStatus(QObject):
53
55
 
54
56
  def set_zoom_factor(self, zoom_factor):
55
57
  self.zoom_factor = zoom_factor
58
+ self.set_zoom_factor_requested.emit(zoom_factor)
56
59
 
57
60
  def set_min_scale(self, min_scale):
58
61
  self.min_scale = min_scale
@@ -7,13 +7,16 @@ from .sidebyside_view import SideBySideView, TopBottomView
7
7
 
8
8
 
9
9
  class ImageViewer(QWidget):
10
- def __init__(self, layer_collection, parent=None):
10
+ def __init__(self, layer_collection, brush_tool, paint_area_manager, parent=None):
11
11
  super().__init__(parent)
12
12
  self.status = ImageViewStatus()
13
13
  self._strategies = {
14
- 'overlaid': OverlaidView(layer_collection, self.status, self),
15
- 'sidebyside': SideBySideView(layer_collection, self.status, self),
16
- 'topbottom': TopBottomView(layer_collection, self.status, self)
14
+ 'overlaid':
15
+ OverlaidView(layer_collection, self.status, brush_tool, paint_area_manager, self),
16
+ 'sidebyside':
17
+ SideBySideView(layer_collection, self.status, brush_tool, paint_area_manager, self),
18
+ 'topbottom':
19
+ TopBottomView(layer_collection, self.status, brush_tool, paint_area_manager, self)
17
20
  }
18
21
  for strategy in self._strategies.values():
19
22
  strategy.hide()
@@ -48,12 +51,18 @@ class ImageViewer(QWidget):
48
51
  def set_master_image_np(self, img):
49
52
  self.strategy.set_master_image_np(img)
50
53
 
54
+ def arrange_images(self):
55
+ self.strategy.arrange_images()
56
+
51
57
  def show_master(self):
52
58
  self.strategy.show_master()
53
59
 
54
60
  def show_current(self):
55
61
  self.strategy.show_current()
56
62
 
63
+ def update_master_display_area(self):
64
+ self.strategy.update_master_display_area()
65
+
57
66
  def update_master_display(self):
58
67
  self.strategy.update_master_display()
59
68
 
@@ -122,12 +131,11 @@ class ImageViewer(QWidget):
122
131
  st.set_cursor_style(style)
123
132
 
124
133
  def connect_signals(
125
- self, handle_temp_view, begin_copy_brush_area, continue_copy_brush_area,
126
- end_copy_brush_area, handle_brush_size_change):
134
+ self, handle_temp_view, end_copy_brush_area,
135
+ handle_brush_size_change, handle_needs_update):
127
136
  for st in self._strategies.values():
128
137
  st.temp_view_requested.connect(handle_temp_view)
129
- st.brush_operation_started.connect(begin_copy_brush_area)
130
- st.brush_operation_continued.connect(continue_copy_brush_area)
131
- st.brush_operation_ended.connect(end_copy_brush_area)
138
+ st.end_copy_brush_area_requested.connect(end_copy_brush_area)
132
139
  st.brush_size_change_requested.connect(handle_brush_size_change)
140
+ st.needs_update_requested.connect(handle_needs_update)
133
141
  st.setFocusPolicy(Qt.StrongFocus)
@@ -1,13 +1,16 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718, R0904, E1101
2
2
  import os
3
3
  import traceback
4
4
  import numpy as np
5
- from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
5
+ import cv2
6
+ from PySide6.QtWidgets import (QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog,
7
+ QApplication, QProgressBar)
6
8
  from PySide6.QtGui import QGuiApplication, QCursor
7
9
  from PySide6.QtCore import Qt, QObject, QTimer, Signal
10
+ from .. algorithms.exif import get_exif, write_image_with_exif_data
8
11
  from .file_loader import FileLoader
9
12
  from .exif_data import ExifData
10
- from .io_manager import IOManager, FileMultilayerSaver
13
+ from .io_threads import FileMultilayerSaver, FrameImporter
11
14
  from .layer_collection import LayerCollectionHandler
12
15
 
13
16
 
@@ -22,13 +25,11 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
22
25
  def __init__(self, layer_collection, undo_manager, parent):
23
26
  QObject.__init__(self, parent)
24
27
  LayerCollectionHandler.__init__(self)
25
- self.io_manager = IOManager(layer_collection)
26
28
  self.undo_manager = undo_manager
27
29
  self.set_layer_collection(layer_collection)
28
30
  self.loader_thread = None
29
31
  self.display_manager = None
30
32
  self.image_viewer = None
31
- self.blank_layer = None
32
33
  self.loading_dialog = None
33
34
  self.loading_timer = None
34
35
  self.exif_dialog = None
@@ -37,6 +38,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
37
38
  self.saving_timer = None
38
39
  self.current_file_path_master = ''
39
40
  self.current_file_path_multi = ''
41
+ self.frame_importer_thread = None
42
+ self.frame_loading_dialog = None
43
+ self.frame_loading_timer = None
44
+ self.progress_label = None
45
+ self.progress_bar = None
46
+ self.exif_data = None
47
+ self.exif_path = ''
40
48
 
41
49
  def current_file_path(self):
42
50
  return self.current_file_path_master if self.save_master_only.isChecked() \
@@ -57,8 +65,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
57
65
  self.set_layer_labels(labels)
58
66
  self.set_master_layer(master_layer)
59
67
  self.image_viewer.set_master_image_np(master_layer)
68
+ self.set_blank_layer()
60
69
  self.undo_manager.reset()
61
- self.blank_layer = np.zeros(master_layer.shape[:2])
62
70
  self.finish_loading_setup(f"Loaded: {self.current_file_path()}")
63
71
  self.image_viewer.reset_zoom()
64
72
 
@@ -72,7 +80,40 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
72
80
  self.current_file_path_multi = ''
73
81
  self.status_message_requested.emit(f"Error loading: {self.current_file_path()}")
74
82
 
75
- def on_multilayer_save_success(self):
83
+ def on_frames_imported(self, stack, labels, master):
84
+ QApplication.restoreOverrideCursor()
85
+ self.frame_loading_timer.stop()
86
+ self.frame_loading_dialog.hide()
87
+ self.frame_loading_dialog.deleteLater()
88
+ empty_viewer = self.image_viewer.empty()
89
+ self.image_viewer.set_master_image_np(master)
90
+ if self.layer_stack() is None and len(stack) > 0:
91
+ self.set_layer_stack(np.array(stack))
92
+ if labels is None:
93
+ labels = self.layer_labels()
94
+ else:
95
+ self.set_layer_labels(labels)
96
+ self.set_master_layer(master)
97
+ self.set_blank_layer()
98
+ else:
99
+ if labels is None:
100
+ labels = self.layer_labels()
101
+ for img, label in zip(stack, labels):
102
+ self.add_layer_label(label)
103
+ self.add_layer(img)
104
+ self.finish_loading_setup("Selected frames imported")
105
+ if empty_viewer:
106
+ self.image_viewer.update_master_display()
107
+
108
+ def on_frames_import_error(self, error_msg):
109
+ QApplication.restoreOverrideCursor()
110
+ self.frame_loading_timer.stop()
111
+ self.frame_loading_dialog.hide()
112
+ self.frame_loading_dialog.deleteLater()
113
+ QMessageBox.critical(self.parent(), "Import Error", error_msg)
114
+ self.status_message_requested.emit("Error importing frames")
115
+
116
+ def on_multilayer_saved(self):
76
117
  QApplication.restoreOverrideCursor()
77
118
  self.saving_timer.stop()
78
119
  self.saving_dialog.hide()
@@ -90,6 +131,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
90
131
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {error_msg}")
91
132
 
92
133
  def open_file(self, file_paths=None):
134
+ self.cleanup_old_threads()
93
135
  if file_paths is None:
94
136
  file_paths, _ = QFileDialog.getOpenFileNames(
95
137
  self.parent(), "Open Image", "",
@@ -128,37 +170,37 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
128
170
  "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
129
171
  if file_paths:
130
172
  self.import_frames_from_files(file_paths)
131
- self.status_message_requested.emit("Imported selected frames")
132
173
 
133
174
  def import_frames_from_files(self, file_paths):
134
- empty_viewer = self.image_viewer.empty()
135
- try:
136
- stack, labels, master = self.io_manager.import_frames(file_paths)
137
- except Exception as e:
138
- msg = QMessageBox()
139
- msg.setIcon(QMessageBox.Critical)
140
- msg.setWindowTitle("Import error")
141
- msg.setText(str(e))
142
- msg.exec()
143
- return
144
- self.image_viewer.set_master_image_np(master)
145
- if self.layer_stack() is None and len(stack) > 0:
146
- self.set_layer_stack(np.array(stack))
147
- if labels is None:
148
- labels = self.layer_labels()
149
- else:
150
- self.set_layer_labels(labels)
151
- self.set_master_layer(master)
152
- self.blank_layer = np.zeros(master.shape[:2])
153
- else:
154
- if labels is None:
155
- labels = self.layer_labels()
156
- for img, label in zip(stack, labels):
157
- self.add_layer_label(label)
158
- self.add_layer(img)
159
- self.finish_loading_setup("Selected frames imported")
160
- if empty_viewer:
161
- self.image_viewer.reset_zoom()
175
+ self.cleanup_old_threads()
176
+ QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
177
+ self.frame_loading_dialog = QDialog(self.parent())
178
+ self.frame_loading_dialog.setWindowTitle("Loading Frames")
179
+ self.frame_loading_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
180
+ self.frame_loading_dialog.setModal(True)
181
+ layout = QVBoxLayout()
182
+ self.progress_label = QLabel("Frames loading...")
183
+ layout.addWidget(self.progress_label)
184
+ self.progress_bar = QProgressBar()
185
+ self.progress_bar.setRange(0, 100)
186
+ self.progress_bar.setValue(0)
187
+ layout.addWidget(self.progress_bar)
188
+ self.frame_loading_dialog.setLayout(layout)
189
+ self.frame_loading_timer = QTimer()
190
+ self.frame_loading_timer.setSingleShot(True)
191
+ self.frame_loading_timer.timeout.connect(self.frame_loading_dialog.show)
192
+ self.frame_loading_timer.start(100)
193
+ self.frame_importer_thread = FrameImporter(file_paths, self.master_layer())
194
+ self.frame_importer_thread.finished.connect(self.on_frames_imported)
195
+ self.frame_importer_thread.error.connect(self.on_frames_import_error)
196
+ self.frame_importer_thread.progress.connect(self.update_import_progress)
197
+ self.frame_importer_thread.start()
198
+
199
+ def update_import_progress(self, percent, filename):
200
+ if hasattr(self, 'progress_bar'):
201
+ self.progress_bar.setValue(percent)
202
+ if hasattr(self, 'progress_label'):
203
+ self.progress_label.setText(f"Loading: {filename} ({percent}%)")
162
204
 
163
205
  def finish_loading_setup(self, message):
164
206
  self.display_manager.update_thumbnails()
@@ -203,6 +245,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
203
245
  self.save_multilayer_to_path(path)
204
246
 
205
247
  def save_multilayer_to_path(self, path):
248
+ self.cleanup_old_threads()
206
249
  try:
207
250
  master_layer = {'Master': self.master_layer().copy()}
208
251
  individual_layers = dict(zip(
@@ -211,8 +254,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
211
254
  ))
212
255
  images_dict = {**master_layer, **individual_layers}
213
256
  self.saver_thread = FileMultilayerSaver(
214
- images_dict, path, exif_path=self.io_manager.exif_path)
215
- self.saver_thread.finished.connect(self.on_multilayer_save_success)
257
+ images_dict, path, exif_path=self.exif_path)
258
+ self.saver_thread.finished.connect(self.on_multilayer_saved)
216
259
  self.saver_thread.error.connect(self.on_multilayer_save_error)
217
260
  QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
218
261
  self.saving_dialog = QDialog(self.parent())
@@ -250,12 +293,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
250
293
 
251
294
  def save_master_to_path(self, path):
252
295
  try:
253
- self.io_manager.save_master(path)
296
+ img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
297
+ write_image_with_exif_data(self.exif_data, img, path)
254
298
  self.current_file_path_master = os.path.abspath(path)
255
- self.mark_as_modified_requested.emit(False)
299
+ # self.mark_as_modified_requested.emit(False)
256
300
  self.update_title_requested.emit()
257
- self.status_message_requested.emit(f"Saved master layer to: {path}")
258
301
  self.add_recent_file_requested.emit(self.current_file_path_master)
302
+ self.status_message_requested.emit(f"Saved master layer to: {path}")
259
303
  except Exception as e:
260
304
  traceback.print_tb(e.__traceback__)
261
305
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
@@ -263,14 +307,14 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
263
307
  def select_exif_path(self):
264
308
  path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
265
309
  if path:
266
- self.io_manager.set_exif_data(path)
310
+ self.exif_path = path
311
+ self.exif_data = get_exif(path)
267
312
  self.status_message_requested.emit(f"EXIF data extracted from {path}.")
268
- self.exif_dialog = ExifData(self.io_manager.exif_data, self.parent())
313
+ self.exif_dialog = ExifData(self.exif_data, self.parent())
269
314
  self.exif_dialog.exec()
270
315
 
271
316
  def close_file(self):
272
317
  self.mark_as_modified_requested.emit(False)
273
- self.blank_layer = None
274
318
  self.layer_collection.reset()
275
319
  self.current_file_path_master = ''
276
320
  self.current_file_path_multi = ''
@@ -281,3 +325,11 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
281
325
  self.update_title_requested.emit()
282
326
  self.set_enabled_file_open_close_actions_requested.emit(False)
283
327
  self.status_message_requested.emit("File closed")
328
+
329
+ def cleanup_old_threads(self):
330
+ if self.loader_thread and self.loader_thread.isFinished():
331
+ self.loader_thread = None
332
+ if self.frame_importer_thread and self.frame_importer_thread.isFinished():
333
+ self.frame_importer_thread = None
334
+ if self.saver_thread and self.saver_thread.isFinished():
335
+ self.saver_thread = None
@@ -0,0 +1,78 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, E1101, W0718, R0903, R0914
2
+
3
+ # import time
4
+ import os
5
+ import traceback
6
+ import cv2
7
+ from PySide6.QtCore import QThread, Signal
8
+ from .. algorithms.utils import read_img, validate_image, get_img_metadata
9
+ from .. algorithms.multilayer import write_multilayer_tiff_from_images
10
+
11
+
12
+ class FileMultilayerSaver(QThread):
13
+ finished = Signal()
14
+ error = Signal(str)
15
+
16
+ def __init__(self, images_dict, path, exif_path=None):
17
+ super().__init__()
18
+ self.images_dict = images_dict
19
+ self.path = path
20
+ self.exif_path = exif_path
21
+
22
+ def run(self):
23
+ try:
24
+ write_multilayer_tiff_from_images(
25
+ self.images_dict, self.path, exif_path=self.exif_path)
26
+ self.finished.emit()
27
+ except Exception as e:
28
+ traceback.print_tb(e.__traceback__)
29
+ self.error.emit(str(e))
30
+
31
+
32
+ class FrameImporter(QThread):
33
+ finished = Signal(object, object, object)
34
+ error = Signal(str)
35
+ progress = Signal(int, str)
36
+
37
+ def __init__(self, file_paths, master_layer):
38
+ super().__init__()
39
+ self.file_paths = file_paths
40
+ self.master_layer = master_layer
41
+
42
+ def run(self):
43
+ try:
44
+ stack = []
45
+ labels = []
46
+ master = None
47
+ current_master = self.master_layer
48
+ shape, dtype = None, None
49
+ if current_master is not None:
50
+ shape, dtype = get_img_metadata(current_master)
51
+ total_files = len(self.file_paths)
52
+ for i, path in enumerate(self.file_paths):
53
+ progress_percent = int((i / total_files) * 100)
54
+ self.progress.emit(progress_percent, os.path.basename(path))
55
+ try:
56
+ label = path.split("/")[-1].split(".")[0]
57
+ img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
58
+ if shape is not None and dtype is not None:
59
+ validate_image(img, shape, dtype)
60
+ else:
61
+ shape, dtype = get_img_metadata(img)
62
+ label_x = label
63
+ counter = 0
64
+ while label_x in labels:
65
+ counter += 1
66
+ label_x = f"{label} ({counter})"
67
+ labels.append(label_x)
68
+ stack.append(img)
69
+ if master is None:
70
+ master = img.copy()
71
+ # Add delay for testing
72
+ # time.sleep(0.2)
73
+ except Exception as e:
74
+ raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
75
+ self.progress.emit(100, "Complete")
76
+ self.finished.emit(stack, labels, master)
77
+ except Exception as e:
78
+ self.error.emit(str(e))
@@ -7,6 +7,7 @@ class LayerCollection:
7
7
  self.master_layer = None
8
8
  self.master_layer_copy = None
9
9
  self.layer_stack = None
10
+ self.blank_layer = None
10
11
  self.layer_labels = []
11
12
  self.current_layer_idx = 0
12
13
  self.sorted_indices = None
@@ -15,6 +16,7 @@ class LayerCollection:
15
16
  self.master_layer = None
16
17
  self.master_layer_copy = None
17
18
  self.layer_stack = None
19
+ self.blank_layer = None
18
20
  self.layer_labels = []
19
21
  self.current_layer_idx = 0
20
22
  self.sorted_indices = None
@@ -62,6 +64,10 @@ class LayerCollection:
62
64
  def set_master_layer(self, img):
63
65
  self.master_layer = img
64
66
 
67
+ def set_blank_layer(self):
68
+ if self.master_layer is not None:
69
+ self.blank_layer = np.zeros(self.master_layer.shape[:2])
70
+
65
71
  def restore_master_layer(self):
66
72
  self.master_layer = self.master_layer_copy.copy()
67
73
 
@@ -122,6 +128,9 @@ class LayerCollectionHandler:
122
128
  def current_layer(self):
123
129
  return self.layer_collection.current_layer()
124
130
 
131
+ def blank_layer(self):
132
+ return self.layer_collection.blank_layer
133
+
125
134
  def layer_stack(self):
126
135
  return self.layer_collection.layer_stack
127
136
 
@@ -152,6 +161,9 @@ class LayerCollectionHandler:
152
161
  def set_master_layer(self, img):
153
162
  self.layer_collection.set_master_layer(img)
154
163
 
164
+ def set_blank_layer(self):
165
+ self.layer_collection.set_blank_layer()
166
+
155
167
  def add_layer_label(self, label):
156
168
  self.layer_collection.add_layer_label(label)
157
169
 
@@ -1,11 +1,11 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, E1101, R0904, R0912, R0914, R0902, E0202
1
+ # pylint: disable=C0114, C0115, C0116, E0611, E1101, R0904, R0912, R0914, R0902, E0202, R0913, R0917
2
2
  from PySide6.QtCore import Qt, QPointF, QEvent, QRectF
3
3
  from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
4
4
 
5
5
 
6
6
  class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
7
- def __init__(self, layer_collection, status, parent):
8
- ViewStrategy.__init__(self, layer_collection, status)
7
+ def __init__(self, layer_collection, status, brush_tool, paint_area_manager, parent):
8
+ ViewStrategy.__init__(self, layer_collection, status, brush_tool, paint_area_manager)
9
9
  ImageGraphicsViewBase.__init__(self, parent)
10
10
  self.scene = self.create_scene(self)
11
11
  self.create_pixmaps()
@@ -121,6 +121,7 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
121
121
  self.pixmap_item_master.setVisible(True)
122
122
  self.pixmap_item_current.setVisible(False)
123
123
  self.show_brush_preview()
124
+ self.enable_paint = True
124
125
  if self.brush_cursor:
125
126
  self.scene.removeItem(self.brush_cursor)
126
127
  self.brush_cursor = self.create_circle(self.scene)
@@ -130,21 +131,28 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
130
131
  self.pixmap_item_master.setVisible(False)
131
132
  self.pixmap_item_current.setVisible(True)
132
133
  self.hide_brush_preview()
134
+ self.enable_paint = False
133
135
  if self.brush_cursor:
134
136
  self.scene.removeItem(self.brush_cursor)
135
137
  self.brush_cursor = self.create_alt_circle(self.scene)
136
138
  self.update_brush_cursor()
137
139
 
140
+ def master_is_visible(self):
141
+ return self.pixmap_item_master.isVisible()
142
+
143
+ def current_is_visible(self):
144
+ return self.pixmap_item_current.isVisible()
145
+
138
146
  def arrange_images(self):
139
147
  if self.empty():
140
148
  return
141
- if self.pixmap_item_master.isVisible():
149
+ if self.master_is_visible():
142
150
  pixmap = self.pixmap_item_master.pixmap()
143
151
  if not pixmap.isNull():
144
152
  self.setSceneRect(QRectF(pixmap.rect()))
145
153
  self.centerOn(self.pixmap_item_master)
146
154
  self.center_image(self)
147
- elif self.pixmap_item_current.isVisible():
155
+ elif self.current_is_visible():
148
156
  pixmap = self.pixmap_item_current.pixmap()
149
157
  if not pixmap.isNull():
150
158
  self.setSceneRect(QRectF(pixmap.rect()))
@@ -0,0 +1,30 @@
1
+ # pylint: disable=C0114, C0115, C0116
2
+ from .. config.gui_constants import gui_constants
3
+
4
+
5
+ class PaintAreaManager:
6
+ def __init__(self):
7
+ self.x_start = None
8
+ self.y_start = None
9
+ self.x_end = None
10
+ self.y_end = None
11
+ self.reset()
12
+
13
+ def reset(self):
14
+ self.x_end = self.y_end = 0
15
+ self.x_start = self.y_start = gui_constants.MAX_UNDO_SIZE
16
+
17
+ def extend(self, x_start, y_start, x_end, y_end):
18
+ self.x_start = min(self.x_start, x_start)
19
+ self.y_start = min(self.y_start, y_start)
20
+ self.x_end = max(self.x_end, x_end)
21
+ self.y_end = max(self.y_end, y_end)
22
+
23
+ def area(self):
24
+ return self.x_start, self.y_start, self.x_end, self.y_end
25
+
26
+ def set_area(self, x_start, y_start, x_end, y_end):
27
+ self.x_start = x_start
28
+ self.y_start = y_start
29
+ self.x_end = x_end
30
+ self.y_end = y_end
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003
1
+ # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003, R0913, R0917
2
2
  import time
3
3
  from PySide6.QtCore import Qt, Signal, QEvent, QRectF
4
4
  from PySide6.QtGui import QCursor
@@ -39,8 +39,8 @@ class ImageGraphicsView(ImageGraphicsViewBase):
39
39
 
40
40
 
41
41
  class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
42
- def __init__(self, layer_collection, status, parent):
43
- ViewStrategy.__init__(self, layer_collection, status)
42
+ def __init__(self, layer_collection, status, brush_tool, paint_area_manager, parent):
43
+ ViewStrategy.__init__(self, layer_collection, status, brush_tool, paint_area_manager)
44
44
  QWidget.__init__(self, parent)
45
45
  self.current_view = ImageGraphicsView(parent)
46
46
  self.master_view = ImageGraphicsView(parent)
@@ -61,6 +61,7 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
61
61
  self.master_view.setFocusPolicy(Qt.NoFocus)
62
62
  self.current_brush_cursor = None
63
63
  self.last_color_update_time_current = 0
64
+ self._updating_scrollbars = False
64
65
 
65
66
  def setup_layout(self):
66
67
  raise NotImplementedError("Subclasses must implement setup_layout")
@@ -78,14 +79,25 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
78
79
  self.master_view.mouse_moved.connect(self.handle_master_mouse_move)
79
80
  self.master_view.mouse_released.connect(self.handle_master_mouse_release)
80
81
  self.master_view.gesture_event.connect(self.handle_gesture_event)
82
+
83
+ def sync_scrollbars(source, target):
84
+ if not self._updating_scrollbars:
85
+ self._updating_scrollbars = True
86
+ target.setValue(source.value())
87
+ self._updating_scrollbars = False
88
+
81
89
  self.current_view.horizontalScrollBar().valueChanged.connect(
82
- self.master_view.horizontalScrollBar().setValue)
90
+ lambda value: sync_scrollbars(self.current_view.horizontalScrollBar(),
91
+ self.master_view.horizontalScrollBar()))
83
92
  self.current_view.verticalScrollBar().valueChanged.connect(
84
- self.master_view.verticalScrollBar().setValue)
93
+ lambda value: sync_scrollbars(self.current_view.verticalScrollBar(),
94
+ self.master_view.verticalScrollBar()))
85
95
  self.master_view.horizontalScrollBar().valueChanged.connect(
86
- self.current_view.horizontalScrollBar().setValue)
96
+ lambda value: sync_scrollbars(self.master_view.horizontalScrollBar(),
97
+ self.current_view.horizontalScrollBar()))
87
98
  self.master_view.verticalScrollBar().valueChanged.connect(
88
- self.current_view.verticalScrollBar().setValue)
99
+ lambda value: sync_scrollbars(self.master_view.verticalScrollBar(),
100
+ self.current_view.verticalScrollBar()))
89
101
  self.current_view.wheel_event.connect(self.handle_wheel_event)
90
102
  self.master_view.wheel_event.connect(self.handle_wheel_event)
91
103
  # pylint: disable=C0103, W0201
@@ -377,28 +389,32 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
377
389
  def set_master_image(self, qimage):
378
390
  self.status.set_master_image(qimage)
379
391
  pixmap = self.status.pixmap_master
380
- self.master_view.setSceneRect(QRectF(pixmap.rect()))
392
+ pixmap_rect = QRectF(pixmap.rect())
393
+ self.pixmap_item_master.setPos(0, 0)
394
+ self.master_view.setSceneRect(pixmap_rect)
395
+ self.master_scene.setSceneRect(pixmap_rect)
381
396
  self.pixmap_item_master.setPixmap(pixmap)
382
- img_width, img_height, scale_factor = self.setup_view_image(self.master_view, pixmap)
397
+ _img_width, _img_height, scale_factor = self.setup_view_image(self.master_view, pixmap)
383
398
  self.master_view.resetTransform()
384
399
  self.master_view.scale(scale_factor, scale_factor)
385
400
  self.master_view.centerOn(self.pixmap_item_master)
386
- center = self.master_scene.sceneRect().center()
387
- self.brush_preview.setPos(max(0, min(center.x(), img_width)),
388
- max(0, min(center.y(), img_height)))
389
- self.master_scene.setSceneRect(QRectF(self.pixmap_item_master.boundingRect()))
390
401
  self.center_image(self.master_view)
391
402
  self.update_cursor_pen_width()
392
403
 
393
404
  def set_current_image(self, qimage):
394
405
  self.status.set_current_image(qimage)
395
406
  pixmap = self.status.pixmap_current
396
- self.current_scene.setSceneRect(QRectF(pixmap.rect()))
407
+ pixmap_rect = QRectF(pixmap.rect())
408
+ self.pixmap_item_current.setPos(0, 0)
409
+ self.current_view.setSceneRect(pixmap_rect)
410
+ self.current_scene.setSceneRect(pixmap_rect)
397
411
  self.pixmap_item_current.setPixmap(pixmap)
412
+ _img_width, _img_height, scale_factor = self.setup_view_image(self.current_view, pixmap)
398
413
  self.current_view.resetTransform()
399
- self.master_view.scale(self.zoom_factor(), self.zoom_factor())
400
- self.current_scene.setSceneRect(QRectF(self.pixmap_item_current.boundingRect()))
414
+ self.current_view.scale(scale_factor, scale_factor)
415
+ self.current_view.centerOn(self.pixmap_item_current)
401
416
  self.center_image(self.current_view)
417
+ self.update_cursor_pen_width()
402
418
 
403
419
  def arrange_images(self):
404
420
  if self.status.empty():
@@ -16,8 +16,7 @@ class TransfromationManager(LayerCollectionHandler):
16
16
  if undoable:
17
17
  try:
18
18
  undo = self.editor.undo_manager
19
- undo.x_start, undo.x_stop = 0, 1
20
- undo.y_start, undo.y_stop = 0, 1
19
+ undo.set_paint_area(0, 1, 0, 1)
21
20
  undo.save_undo_state(self.editor.master_layer(), label)
22
21
  except Exception as e:
23
22
  traceback.print_tb(e.__traceback__)
@@ -26,7 +25,6 @@ class TransfromationManager(LayerCollectionHandler):
26
25
  self.copy_master_layer()
27
26
  self.editor.image_viewer.update_master_display()
28
27
  self.editor.image_viewer.update_current_display()
29
- self.editor.image_viewer.refresh_display()
30
28
  self.editor.display_manager.update_thumbnails()
31
29
  self.editor.mark_as_modified()
32
30