shinestacker 0.4.0__py3-none-any.whl → 1.0.0__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 +1 -1
- shinestacker/algorithms/align.py +4 -12
- shinestacker/algorithms/balance.py +11 -9
- shinestacker/algorithms/depth_map.py +0 -30
- shinestacker/algorithms/utils.py +10 -0
- shinestacker/algorithms/vignetting.py +116 -70
- shinestacker/app/about_dialog.py +101 -12
- shinestacker/app/gui_utils.py +1 -1
- shinestacker/app/help_menu.py +1 -1
- shinestacker/app/main.py +2 -2
- shinestacker/app/project.py +2 -2
- shinestacker/config/constants.py +4 -1
- shinestacker/config/gui_constants.py +10 -9
- shinestacker/gui/action_config.py +5 -561
- shinestacker/gui/action_config_dialog.py +567 -0
- shinestacker/gui/base_form_dialog.py +18 -0
- shinestacker/gui/colors.py +5 -6
- shinestacker/gui/gui_logging.py +0 -1
- shinestacker/gui/gui_run.py +54 -106
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker/gui/ico/shinestacker.svg +60 -0
- shinestacker/gui/main_window.py +276 -367
- shinestacker/gui/menu_manager.py +236 -0
- shinestacker/gui/new_project.py +75 -20
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +248 -165
- shinestacker/gui/project_model.py +2 -7
- shinestacker/gui/tab_widget.py +81 -0
- shinestacker/gui/time_progress_bar.py +95 -0
- shinestacker/retouch/base_filter.py +173 -40
- shinestacker/retouch/brush_preview.py +0 -10
- shinestacker/retouch/brush_tool.py +25 -11
- shinestacker/retouch/denoise_filter.py +5 -44
- shinestacker/retouch/display_manager.py +57 -20
- shinestacker/retouch/exif_data.py +10 -13
- shinestacker/retouch/file_loader.py +1 -1
- shinestacker/retouch/filter_manager.py +1 -4
- shinestacker/retouch/image_editor_ui.py +365 -49
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +96 -43
- shinestacker/retouch/io_manager.py +23 -7
- shinestacker/retouch/layer_collection.py +2 -0
- shinestacker/retouch/shortcuts_help.py +12 -0
- shinestacker/retouch/unsharp_mask_filter.py +10 -10
- shinestacker/retouch/vignetting_filter.py +69 -0
- shinestacker/retouch/white_balance_filter.py +46 -14
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
- shinestacker-1.0.0.dist-info/RECORD +90 -0
- shinestacker/app/app_config.py +0 -22
- shinestacker/gui/actions_window.py +0 -258
- shinestacker/retouch/image_editor.py +0 -201
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.4.0.dist-info/RECORD +0 -87
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,42 +1,50 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904
|
|
2
|
+
import numpy as np
|
|
2
3
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel,
|
|
3
|
-
QListWidget, QSlider)
|
|
4
|
+
QListWidget, QSlider, QMainWindow, QMessageBox)
|
|
4
5
|
from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
|
|
5
6
|
from PySide6.QtCore import Qt
|
|
6
7
|
from PySide6.QtGui import QGuiApplication
|
|
8
|
+
from .. config.constants import constants
|
|
7
9
|
from .. config.gui_constants import gui_constants
|
|
8
|
-
from .image_filters import ImageFilters
|
|
9
10
|
from .image_viewer import ImageViewer
|
|
10
11
|
from .shortcuts_help import ShortcutsHelp
|
|
11
12
|
from .brush import Brush
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
from .brush_tool import BrushTool
|
|
14
|
+
from .layer_collection import LayerCollectionHandler
|
|
15
|
+
from .undo_manager import UndoManager
|
|
16
|
+
from .layer_collection import LayerCollection
|
|
17
|
+
from .io_gui_handler import IOGuiHandler
|
|
18
|
+
from .display_manager import DisplayManager
|
|
19
|
+
from .filter_manager import FilterManager
|
|
20
|
+
from .denoise_filter import DenoiseFilter
|
|
21
|
+
from .unsharp_mask_filter import UnsharpMaskFilter
|
|
22
|
+
from .white_balance_filter import WhiteBalanceFilter
|
|
23
|
+
from .vignetting_filter import VignettingFilter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
25
27
|
def __init__(self):
|
|
26
|
-
|
|
28
|
+
QMainWindow.__init__(self)
|
|
29
|
+
LayerCollectionHandler.__init__(self, LayerCollection())
|
|
30
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
31
|
+
self.undo_manager = UndoManager()
|
|
32
|
+
self.undo_action = None
|
|
33
|
+
self.redo_action = None
|
|
34
|
+
self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
|
|
35
|
+
self.io_gui_handler = None
|
|
36
|
+
self.display_manager = None
|
|
27
37
|
self.brush = Brush()
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
next_layer.activated.connect(self.next_layer)
|
|
38
|
+
self.brush_tool = BrushTool()
|
|
39
|
+
self.modified = False
|
|
40
|
+
self.mask_layer = None
|
|
41
|
+
self.filter_manager = FilterManager(self)
|
|
42
|
+
self.filter_manager.register_filter("Denoise", DenoiseFilter)
|
|
43
|
+
self.filter_manager.register_filter("Unsharp Mask", UnsharpMaskFilter)
|
|
44
|
+
self.filter_manager.register_filter("White Balance", WhiteBalanceFilter)
|
|
45
|
+
self.filter_manager.register_filter("Vignetting Correction", VignettingFilter)
|
|
46
|
+
self.shortcuts_help_dialog = None
|
|
38
47
|
|
|
39
|
-
def setup_ui(self):
|
|
40
48
|
self.update_title()
|
|
41
49
|
self.resize(1400, 900)
|
|
42
50
|
center = QGuiApplication.primaryScreen().geometry().center()
|
|
@@ -69,6 +77,16 @@ class ImageEditorUI(ImageFilters):
|
|
|
69
77
|
|
|
70
78
|
self.brush_size_slider = QSlider(Qt.Horizontal)
|
|
71
79
|
self.brush_size_slider.setRange(0, gui_constants.BRUSH_SIZE_SLIDER_MAX)
|
|
80
|
+
|
|
81
|
+
def brush_size_to_slider(size):
|
|
82
|
+
if size <= gui_constants.BRUSH_SIZES['min']:
|
|
83
|
+
return 0
|
|
84
|
+
if size >= gui_constants.BRUSH_SIZES['max']:
|
|
85
|
+
return gui_constants.BRUSH_SIZE_SLIDER_MAX
|
|
86
|
+
normalized = ((size - gui_constants.BRUSH_SIZES['min']) /
|
|
87
|
+
gui_constants.BRUSH_SIZES['max']) ** (1 / gui_constants.BRUSH_GAMMA)
|
|
88
|
+
return int(normalized * gui_constants.BRUSH_SIZE_SLIDER_MAX)
|
|
89
|
+
|
|
72
90
|
self.brush_size_slider.setValue(brush_size_to_slider(self.brush.size))
|
|
73
91
|
brush_layout.addWidget(self.brush_size_slider)
|
|
74
92
|
|
|
@@ -125,18 +143,28 @@ class ImageEditorUI(ImageFilters):
|
|
|
125
143
|
}
|
|
126
144
|
""")
|
|
127
145
|
master_label.setAlignment(Qt.AlignCenter)
|
|
128
|
-
master_label.setFixedHeight(gui_constants.
|
|
146
|
+
master_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
|
|
129
147
|
side_layout.addWidget(master_label)
|
|
130
148
|
self.master_thumbnail_frame = QFrame()
|
|
149
|
+
self.master_thumbnail_frame.setObjectName("thumbnailContainer")
|
|
150
|
+
self.master_thumbnail_frame.setStyleSheet(
|
|
151
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
131
152
|
self.master_thumbnail_frame.setFrameShape(QFrame.StyledPanel)
|
|
132
153
|
master_thumbnail_layout = QVBoxLayout(self.master_thumbnail_frame)
|
|
133
|
-
master_thumbnail_layout.setContentsMargins(
|
|
154
|
+
master_thumbnail_layout.setContentsMargins(8, 8, 8, 8)
|
|
134
155
|
self.master_thumbnail_label = QLabel()
|
|
135
156
|
self.master_thumbnail_label.setAlignment(Qt.AlignCenter)
|
|
136
|
-
self.master_thumbnail_label.
|
|
137
|
-
gui_constants.
|
|
157
|
+
self.master_thumbnail_label.setFixedWidth(
|
|
158
|
+
gui_constants.UI_SIZES['thumbnail_width'])
|
|
138
159
|
self.master_thumbnail_label.mousePressEvent = \
|
|
139
160
|
lambda e: self.display_manager.set_view_master()
|
|
161
|
+
self.master_thumbnail_label.setMouseTracking(True)
|
|
162
|
+
|
|
163
|
+
def label_clicked(event):
|
|
164
|
+
if event.button() == Qt.LeftButton:
|
|
165
|
+
self.toggle_view_master_individual()
|
|
166
|
+
|
|
167
|
+
self.master_thumbnail_label.mousePressEvent = label_clicked
|
|
140
168
|
master_thumbnail_layout.addWidget(self.master_thumbnail_label)
|
|
141
169
|
side_layout.addWidget(self.master_thumbnail_frame)
|
|
142
170
|
side_layout.addSpacing(10)
|
|
@@ -152,7 +180,7 @@ class ImageEditorUI(ImageFilters):
|
|
|
152
180
|
}
|
|
153
181
|
""")
|
|
154
182
|
layers_label.setAlignment(Qt.AlignCenter)
|
|
155
|
-
layers_label.setFixedHeight(gui_constants.
|
|
183
|
+
layers_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
|
|
156
184
|
side_layout.addWidget(layers_label)
|
|
157
185
|
self.thumbnail_list = QListWidget()
|
|
158
186
|
self.thumbnail_list.setFocusPolicy(Qt.StrongFocus)
|
|
@@ -202,20 +230,44 @@ class ImageEditorUI(ImageFilters):
|
|
|
202
230
|
layout.addWidget(control_panel, 0)
|
|
203
231
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
204
232
|
layout.setSpacing(2)
|
|
205
|
-
|
|
233
|
+
self.display_manager = DisplayManager(
|
|
234
|
+
self.layer_collection, self.image_viewer,
|
|
235
|
+
self.master_thumbnail_label, self.thumbnail_list, parent=self)
|
|
236
|
+
self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
|
|
237
|
+
self.display_manager.status_message_requested.connect(self.show_status_message)
|
|
238
|
+
self.display_manager.cursor_preview_state_changed.connect(
|
|
239
|
+
lambda state: setattr(self.image_viewer, 'allow_cursor_preview', state))
|
|
240
|
+
self.io_gui_handler.status_message_requested.connect(self.show_status_message)
|
|
241
|
+
self.io_gui_handler.update_title_requested.connect(self.update_title)
|
|
242
|
+
self.io_gui_handler.mark_as_modified_requested.connect(self.mark_as_modified)
|
|
243
|
+
self.io_gui_handler.change_layer_requested.connect(self.change_layer)
|
|
244
|
+
self.brush_tool.setup_ui(self.brush, self.brush_preview, self.image_viewer,
|
|
245
|
+
self.brush_size_slider, self.hardness_slider, self.opacity_slider,
|
|
246
|
+
self.flow_slider)
|
|
247
|
+
self.image_viewer.brush = self.brush_tool.brush
|
|
248
|
+
self.brush_tool.update_brush_thumb()
|
|
249
|
+
self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
|
|
250
|
+
self.image_viewer.display_manager = self.display_manager
|
|
206
251
|
|
|
207
|
-
def setup_menu(self):
|
|
208
252
|
menubar = self.menuBar()
|
|
209
253
|
file_menu = menubar.addMenu("&File")
|
|
210
254
|
file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.
|
|
214
|
-
self.
|
|
215
|
-
self.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
file_menu.addAction(
|
|
255
|
+
self.save_action = QAction("&Save", self)
|
|
256
|
+
self.save_action.setShortcut("Ctrl+S")
|
|
257
|
+
self.save_action.triggered.connect(self.io_gui_handler.save_file)
|
|
258
|
+
file_menu.addAction(self.save_action)
|
|
259
|
+
self.save_as_action = QAction("Save &As...", self)
|
|
260
|
+
self.save_as_action.setShortcut("Ctrl+Shift+S")
|
|
261
|
+
self.save_as_action.triggered.connect(self.io_gui_handler.save_file_as)
|
|
262
|
+
file_menu.addAction(self.save_as_action)
|
|
263
|
+
self.io_gui_handler.save_master_only = QAction("Save Master &Only", self)
|
|
264
|
+
self.io_gui_handler.save_master_only.setCheckable(True)
|
|
265
|
+
self.io_gui_handler.save_master_only.setChecked(True)
|
|
266
|
+
self.io_gui_handler.save_master_only.triggered.connect(self.save_master_only)
|
|
267
|
+
file_menu.addAction(self.io_gui_handler.save_master_only)
|
|
268
|
+
self.save_actions_set_enabled(False)
|
|
269
|
+
|
|
270
|
+
file_menu.addAction("&Close", self.close_file, "Ctrl+W")
|
|
219
271
|
file_menu.addSeparator()
|
|
220
272
|
file_menu.addAction("&Import frames", self.io_gui_handler.import_frames)
|
|
221
273
|
file_menu.addAction("Import &EXIF data", self.io_gui_handler.select_exif_path)
|
|
@@ -271,15 +323,21 @@ class ImageEditorUI(ImageFilters):
|
|
|
271
323
|
|
|
272
324
|
view_master_action = QAction("View Master", self)
|
|
273
325
|
view_master_action.setShortcut("M")
|
|
274
|
-
view_master_action.triggered.connect(self.
|
|
326
|
+
view_master_action.triggered.connect(self.set_view_master)
|
|
275
327
|
view_menu.addAction(view_master_action)
|
|
276
328
|
|
|
277
329
|
view_individual_action = QAction("View Individual", self)
|
|
278
330
|
view_individual_action.setShortcut("L")
|
|
279
|
-
view_individual_action.triggered.connect(self.
|
|
331
|
+
view_individual_action.triggered.connect(self.set_view_individual)
|
|
280
332
|
view_menu.addAction(view_individual_action)
|
|
281
333
|
view_menu.addSeparator()
|
|
282
334
|
|
|
335
|
+
toggle_view_master_individual_action = QAction("View Individual", self)
|
|
336
|
+
toggle_view_master_individual_action.setShortcut("T")
|
|
337
|
+
toggle_view_master_individual_action.triggered.connect(self.toggle_view_master_individual)
|
|
338
|
+
view_menu.addAction(toggle_view_master_individual_action)
|
|
339
|
+
view_menu.addSeparator()
|
|
340
|
+
|
|
283
341
|
sort_asc_action = QAction("Sort Layers A-Z", self)
|
|
284
342
|
sort_asc_action.triggered.connect(lambda: self.sort_layers('asc'))
|
|
285
343
|
view_menu.addAction(sort_asc_action)
|
|
@@ -327,16 +385,270 @@ class ImageEditorUI(ImageFilters):
|
|
|
327
385
|
white_balance_action = QAction("White Balance", self)
|
|
328
386
|
white_balance_action.triggered.connect(self.white_balance)
|
|
329
387
|
filter_menu.addAction(white_balance_action)
|
|
388
|
+
vignetting_action = QAction("Vignetting correction", self)
|
|
389
|
+
vignetting_action.triggered.connect(self.vignetting_correction)
|
|
390
|
+
filter_menu.addAction(vignetting_action)
|
|
330
391
|
|
|
331
392
|
help_menu = menubar.addMenu("&Help")
|
|
332
393
|
help_menu.setObjectName("Help")
|
|
333
394
|
shortcuts_help_action = QAction("Shortcuts and mouse", self)
|
|
334
|
-
|
|
395
|
+
|
|
396
|
+
def shortcuts_help():
|
|
397
|
+
self.shortcuts_help_dialog = ShortcutsHelp(self)
|
|
398
|
+
self.shortcuts_help_dialog.exec()
|
|
399
|
+
|
|
400
|
+
shortcuts_help_action.triggered.connect(shortcuts_help)
|
|
335
401
|
help_menu.addAction(shortcuts_help_action)
|
|
336
402
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
self.
|
|
403
|
+
prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
|
|
404
|
+
prev_layer.activated.connect(self.prev_layer)
|
|
405
|
+
next_layer = QShortcut(QKeySequence(Qt.Key_Down), self, context=Qt.ApplicationShortcut)
|
|
406
|
+
next_layer.activated.connect(self.next_layer)
|
|
407
|
+
self.installEventFilter(self)
|
|
408
|
+
|
|
409
|
+
def update_title(self):
|
|
410
|
+
title = constants.APP_TITLE
|
|
411
|
+
if self.io_gui_handler is not None:
|
|
412
|
+
path = self.io_gui_handler.current_file_path()
|
|
413
|
+
if path != '':
|
|
414
|
+
title += f" - {path.split('/')[-1]}"
|
|
415
|
+
if self.modified:
|
|
416
|
+
title += " *"
|
|
417
|
+
self.window().setWindowTitle(title)
|
|
418
|
+
|
|
419
|
+
def show_status_message(self, message):
|
|
420
|
+
self.statusBar().showMessage(message)
|
|
421
|
+
|
|
422
|
+
def mark_as_modified(self, value=True):
|
|
423
|
+
self.modified = value
|
|
424
|
+
self.save_actions_set_enabled(value)
|
|
425
|
+
self.update_title()
|
|
426
|
+
|
|
427
|
+
def check_unsaved_changes(self) -> bool:
|
|
428
|
+
if self.modified:
|
|
429
|
+
reply = QMessageBox.question(
|
|
430
|
+
self, "Unsaved Changes",
|
|
431
|
+
"The image stack has unsaved changes. Do you want to continue?",
|
|
432
|
+
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
|
|
433
|
+
)
|
|
434
|
+
if reply == QMessageBox.Save:
|
|
435
|
+
self.io_gui_handler.save_file()
|
|
436
|
+
return True
|
|
437
|
+
if reply == QMessageBox.Discard:
|
|
438
|
+
return True
|
|
439
|
+
return False
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
# pylint: disable=C0103
|
|
443
|
+
def keyPressEvent(self, event):
|
|
444
|
+
if self.image_viewer.empty:
|
|
445
|
+
return
|
|
446
|
+
if event.text() == '[':
|
|
447
|
+
self.brush_tool.decrease_brush_size()
|
|
448
|
+
return
|
|
449
|
+
if event.text() == ']':
|
|
450
|
+
self.brush_tool.increase_brush_size()
|
|
451
|
+
return
|
|
452
|
+
if event.text() == '{':
|
|
453
|
+
self.brush_tool.decrease_brush_hardness()
|
|
454
|
+
return
|
|
455
|
+
if event.text() == '}':
|
|
456
|
+
self.brush_tool.increase_brush_hardness()
|
|
457
|
+
return
|
|
458
|
+
super().keyPressEvent(event)
|
|
459
|
+
# pylint: enable=C0103
|
|
460
|
+
|
|
461
|
+
def save_master_only(self, _checked):
|
|
462
|
+
self.update_title()
|
|
463
|
+
|
|
464
|
+
def sort_layers(self, order):
|
|
465
|
+
self.sort_layers(order)
|
|
466
|
+
self.display_manager.update_thumbnails()
|
|
467
|
+
self.change_layer(self.current_layer())
|
|
468
|
+
|
|
469
|
+
def change_layer(self, layer_idx):
|
|
470
|
+
if 0 <= layer_idx < self.number_of_layers():
|
|
471
|
+
view_state = self.image_viewer.get_view_state()
|
|
472
|
+
self.set_current_layer_idx(layer_idx)
|
|
473
|
+
self.display_manager.display_current_view()
|
|
474
|
+
self.image_viewer.set_view_state(view_state)
|
|
475
|
+
self.thumbnail_list.setCurrentRow(layer_idx)
|
|
476
|
+
self.thumbnail_list.setFocus()
|
|
477
|
+
self.image_viewer.update_brush_cursor()
|
|
478
|
+
self.image_viewer.setFocus()
|
|
479
|
+
|
|
480
|
+
def prev_layer(self):
|
|
481
|
+
if self.layer_stack() is not None:
|
|
482
|
+
new_idx = max(0, self.current_layer_idx() - 1)
|
|
483
|
+
if new_idx != self.current_layer_idx():
|
|
484
|
+
self.change_layer(new_idx)
|
|
485
|
+
self.display_manager.highlight_thumbnail(new_idx)
|
|
486
|
+
|
|
487
|
+
def next_layer(self):
|
|
488
|
+
if self.layer_stack() is not None:
|
|
489
|
+
new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
|
|
490
|
+
if new_idx != self.current_layer_idx():
|
|
491
|
+
self.change_layer(new_idx)
|
|
492
|
+
self.display_manager.highlight_thumbnail(new_idx)
|
|
493
|
+
|
|
494
|
+
def copy_layer_to_master(self):
|
|
495
|
+
if self.layer_stack() is None or self.master_layer() is None:
|
|
496
|
+
return
|
|
497
|
+
reply = QMessageBox.question(
|
|
498
|
+
self,
|
|
499
|
+
"Confirm Copy",
|
|
500
|
+
"Warning: the current master layer will be erased.\n\nDo you want to continue?",
|
|
501
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
502
|
+
QMessageBox.No
|
|
503
|
+
)
|
|
504
|
+
if reply == QMessageBox.Yes:
|
|
505
|
+
self.set_master_layer(self.current_layer().copy())
|
|
506
|
+
self.master_layer().setflags(write=True)
|
|
507
|
+
self.display_manager.display_current_view()
|
|
508
|
+
self.display_manager.update_thumbnails()
|
|
509
|
+
self.mark_as_modified()
|
|
510
|
+
self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
|
|
511
|
+
|
|
512
|
+
def copy_brush_area_to_master(self, view_pos):
|
|
513
|
+
if self.layer_stack() is None or self.number_of_layers() == 0 \
|
|
514
|
+
or not self.display_manager.allow_cursor_preview():
|
|
515
|
+
return
|
|
516
|
+
area = self.brush_tool.apply_brush_operation(
|
|
517
|
+
self.master_layer_copy(),
|
|
518
|
+
self.current_layer(),
|
|
519
|
+
self.master_layer(), self.mask_layer,
|
|
520
|
+
view_pos)
|
|
521
|
+
self.undo_manager.extend_undo_area(*area)
|
|
522
|
+
|
|
523
|
+
def begin_copy_brush_area(self, pos):
|
|
524
|
+
if self.display_manager.allow_cursor_preview():
|
|
525
|
+
self.mask_layer = self.io_gui_handler.blank_layer.copy()
|
|
526
|
+
self.copy_master_layer()
|
|
527
|
+
self.undo_manager.reset_undo_area()
|
|
528
|
+
self.copy_brush_area_to_master(pos)
|
|
529
|
+
self.display_manager.needs_update = True
|
|
530
|
+
if not self.display_manager.update_timer.isActive():
|
|
531
|
+
self.display_manager.update_timer.start()
|
|
532
|
+
self.mark_as_modified()
|
|
533
|
+
|
|
534
|
+
def continue_copy_brush_area(self, pos):
|
|
535
|
+
if self.display_manager.allow_cursor_preview():
|
|
536
|
+
self.copy_brush_area_to_master(pos)
|
|
537
|
+
self.display_manager.needs_update = True
|
|
538
|
+
if not self.display_manager.update_timer.isActive():
|
|
539
|
+
self.display_manager.update_timer.start()
|
|
540
|
+
self.mark_as_modified()
|
|
541
|
+
|
|
542
|
+
def end_copy_brush_area(self):
|
|
543
|
+
if self.display_manager.update_timer.isActive():
|
|
544
|
+
self.display_manager.display_master_layer()
|
|
545
|
+
self.display_manager.update_master_thumbnail()
|
|
546
|
+
self.undo_manager.save_undo_state(self.master_layer_copy(), 'Brush Stroke')
|
|
547
|
+
self.display_manager.update_timer.stop()
|
|
548
|
+
self.mark_as_modified()
|
|
549
|
+
|
|
550
|
+
def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
|
|
551
|
+
if self.undo_action:
|
|
552
|
+
if has_undo:
|
|
553
|
+
self.undo_action.setText(f"Undo {undo_desc}")
|
|
554
|
+
self.undo_action.setEnabled(True)
|
|
555
|
+
else:
|
|
556
|
+
self.undo_action.setText("Undo")
|
|
557
|
+
self.undo_action.setEnabled(False)
|
|
558
|
+
if self.redo_action:
|
|
559
|
+
if has_redo:
|
|
560
|
+
self.redo_action.setText(f"Redo {redo_desc}")
|
|
561
|
+
self.redo_action.setEnabled(True)
|
|
562
|
+
else:
|
|
563
|
+
self.redo_action.setText("Redo")
|
|
564
|
+
self.redo_action.setEnabled(False)
|
|
565
|
+
|
|
566
|
+
def denoise_filter(self):
|
|
567
|
+
self.filter_manager.apply("Denoise")
|
|
568
|
+
|
|
569
|
+
def unsharp_mask(self):
|
|
570
|
+
self.filter_manager.apply("Unsharp Mask")
|
|
571
|
+
|
|
572
|
+
def white_balance(self, init_val=None):
|
|
573
|
+
self.filter_manager.apply("White Balance", init_val=init_val or (128, 128, 128))
|
|
574
|
+
|
|
575
|
+
def vignetting_correction(self):
|
|
576
|
+
self.filter_manager.apply("Vignetting Correction")
|
|
577
|
+
|
|
578
|
+
def connect_preview_toggle(self, preview_check, do_preview, restore_original):
|
|
579
|
+
def on_toggled(checked):
|
|
580
|
+
if checked:
|
|
581
|
+
do_preview()
|
|
582
|
+
else:
|
|
583
|
+
restore_original()
|
|
584
|
+
preview_check.toggled.connect(on_toggled)
|
|
585
|
+
|
|
586
|
+
def get_pixel_color_at(self, pos, radius=None):
|
|
587
|
+
item_pos = self.image_viewer.position_on_image(pos)
|
|
588
|
+
x = int(item_pos.x())
|
|
589
|
+
y = int(item_pos.y())
|
|
590
|
+
master_layer = self.master_layer()
|
|
591
|
+
if (0 <= x < self.master_layer().shape[1]) and \
|
|
592
|
+
(0 <= y < self.master_layer().shape[0]):
|
|
593
|
+
if radius is None:
|
|
594
|
+
radius = int(self.brush.size)
|
|
595
|
+
if radius > 0:
|
|
596
|
+
y_indices, x_indices = np.ogrid[-radius:radius + 1, -radius:radius + 1]
|
|
597
|
+
mask = x_indices**2 + y_indices**2 <= radius**2
|
|
598
|
+
x0 = max(0, x - radius)
|
|
599
|
+
x1 = min(master_layer.shape[1], x + radius + 1)
|
|
600
|
+
y0 = max(0, y - radius)
|
|
601
|
+
y1 = min(master_layer.shape[0], y + radius + 1)
|
|
602
|
+
mask = mask[radius - (y - y0): radius + (y1 - y),
|
|
603
|
+
radius - (x - x0): radius + (x1 - x)]
|
|
604
|
+
region = master_layer[y0:y1, x0:x1]
|
|
605
|
+
if region.size == 0:
|
|
606
|
+
pixel = master_layer[y, x]
|
|
607
|
+
else:
|
|
608
|
+
if region.ndim == 3:
|
|
609
|
+
pixel = [region[:, :, c][mask].mean() for c in range(region.shape[2])]
|
|
610
|
+
else:
|
|
611
|
+
pixel = region[mask].mean()
|
|
612
|
+
else:
|
|
613
|
+
pixel = self.master_layer()[y, x]
|
|
614
|
+
if np.isscalar(pixel):
|
|
615
|
+
pixel = [pixel, pixel, pixel]
|
|
616
|
+
pixel = [np.float32(x) for x in pixel]
|
|
617
|
+
if master_layer.dtype == np.uint16:
|
|
618
|
+
pixel = [x / 256.0 for x in pixel]
|
|
619
|
+
return tuple(int(v) for v in pixel)
|
|
620
|
+
return (0, 0, 0)
|
|
621
|
+
|
|
622
|
+
def highlight_master_thumbnail(self):
|
|
623
|
+
self.master_thumbnail_frame.setStyleSheet(
|
|
624
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
625
|
+
|
|
626
|
+
def save_actions_set_enabled(self, enabled):
|
|
627
|
+
self.save_action.setEnabled(enabled)
|
|
628
|
+
self.save_as_action.setEnabled(enabled)
|
|
629
|
+
self.io_gui_handler.save_master_only.setEnabled(enabled)
|
|
630
|
+
|
|
631
|
+
def close_file(self):
|
|
632
|
+
if self.check_unsaved_changes():
|
|
633
|
+
self.io_gui_handler.close_file()
|
|
634
|
+
self.set_master_layer(None)
|
|
635
|
+
self.mark_as_modified(False)
|
|
636
|
+
|
|
637
|
+
def set_view_master(self):
|
|
638
|
+
self.display_manager.set_view_master()
|
|
639
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
640
|
+
self.highlight_master_thumbnail()
|
|
641
|
+
|
|
642
|
+
def set_view_individual(self):
|
|
643
|
+
self.display_manager.set_view_individual()
|
|
644
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
|
|
645
|
+
self.highlight_master_thumbnail()
|
|
646
|
+
|
|
647
|
+
def toggle_view_master_individual(self):
|
|
648
|
+
if self.display_manager.view_mode == 'master':
|
|
649
|
+
self.set_view_individual()
|
|
650
|
+
else:
|
|
651
|
+
self.set_view_master()
|
|
340
652
|
|
|
341
653
|
def toggle_fullscreen(self, checked):
|
|
342
654
|
if checked:
|
|
@@ -365,8 +677,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
365
677
|
def handle_temp_view(self, start):
|
|
366
678
|
if start:
|
|
367
679
|
self.display_manager.start_temp_view()
|
|
680
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
|
|
681
|
+
self.highlight_master_thumbnail()
|
|
368
682
|
else:
|
|
369
683
|
self.display_manager.end_temp_view()
|
|
684
|
+
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
685
|
+
self.highlight_master_thumbnail()
|
|
370
686
|
|
|
371
687
|
def handle_brush_size_change(self, delta):
|
|
372
688
|
if delta > 0:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914, R0912
|
|
2
2
|
import math
|
|
3
|
-
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
|
|
4
|
-
|
|
3
|
+
from PySide6.QtWidgets import (QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
|
|
4
|
+
QGraphicsEllipseItem)
|
|
5
|
+
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor
|
|
5
6
|
from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal, QEvent
|
|
6
7
|
from .. config.gui_constants import gui_constants
|
|
7
8
|
from .brush_preview import BrushPreviewItem
|
|
@@ -64,8 +65,9 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
64
65
|
pixmap = QPixmap.fromImage(qimage)
|
|
65
66
|
self.pixmap_item.setPixmap(pixmap)
|
|
66
67
|
self.setSceneRect(QRectF(pixmap.rect()))
|
|
67
|
-
img_width = pixmap.width()
|
|
68
|
-
self.min_scale = gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width
|
|
68
|
+
img_width, img_height = pixmap.width(), pixmap.height()
|
|
69
|
+
self.min_scale = min(gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width,
|
|
70
|
+
gui_constants.MIN_ZOOMED_IMG_HEIGHT / img_height)
|
|
69
71
|
self.max_scale = gui_constants.MAX_ZOOMED_IMG_PX_SIZE
|
|
70
72
|
if self.zoom_factor == 1.0:
|
|
71
73
|
self.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
|
|
@@ -221,7 +223,6 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
221
223
|
self.zoom_factor = new_scale
|
|
222
224
|
self.update_brush_cursor()
|
|
223
225
|
else: # Touchpad event - fallback for systems without gesture recognition
|
|
224
|
-
# Handle touchpad panning (two-finger scroll)
|
|
225
226
|
if not self.control_pressed:
|
|
226
227
|
delta = event.pixelDelta() or event.angleDelta() / 8
|
|
227
228
|
if delta:
|
|
@@ -318,6 +319,9 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
318
319
|
self.setCursor(Qt.BlankCursor)
|
|
319
320
|
pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
|
|
320
321
|
brush = QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner']))
|
|
322
|
+
for item in self.scene.items():
|
|
323
|
+
if isinstance(item, QGraphicsEllipseItem):
|
|
324
|
+
self.scene.removeItem(item)
|
|
321
325
|
self.brush_cursor = self.scene.addEllipse(
|
|
322
326
|
0, 0, self.brush.size, self.brush.size, pen, brush)
|
|
323
327
|
self.brush_cursor.setZValue(1000)
|
|
@@ -369,12 +373,6 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
369
373
|
gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor))
|
|
370
374
|
self.brush_cursor.setBrush(QBrush(gradient))
|
|
371
375
|
|
|
372
|
-
def setup_shortcuts(self):
|
|
373
|
-
prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
|
|
374
|
-
prev_layer.activated.connect(self.prev_layer)
|
|
375
|
-
next_layer = QShortcut(QKeySequence(Qt.Key_Down), self, context=Qt.ApplicationShortcut)
|
|
376
|
-
next_layer.activated.connect(self.next_layer)
|
|
377
|
-
|
|
378
376
|
def zoom_in(self):
|
|
379
377
|
if self.empty:
|
|
380
378
|
return
|
|
@@ -398,6 +396,11 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
398
396
|
def reset_zoom(self):
|
|
399
397
|
if self.empty:
|
|
400
398
|
return
|
|
399
|
+
self.pinch_start_scale = 1.0
|
|
400
|
+
self.last_scroll_pos = QPointF()
|
|
401
|
+
self.gesture_active = False
|
|
402
|
+
self.pinch_center_view = None
|
|
403
|
+
self.pinch_center_scene = None
|
|
401
404
|
self.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
|
|
402
405
|
self.zoom_factor = self.get_current_scale()
|
|
403
406
|
self.zoom_factor = max(self.min_scale, min(self.max_scale, self.zoom_factor))
|
|
@@ -440,3 +443,23 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
|
440
443
|
scene_pos = self.mapToScene(pos)
|
|
441
444
|
item_pos = self.pixmap_item.mapFromScene(scene_pos)
|
|
442
445
|
return item_pos
|
|
446
|
+
|
|
447
|
+
def get_visible_image_region(self):
|
|
448
|
+
if self.empty:
|
|
449
|
+
return None
|
|
450
|
+
view_rect = self.viewport().rect()
|
|
451
|
+
scene_rect = self.mapToScene(view_rect).boundingRect()
|
|
452
|
+
image_rect = self.pixmap_item.mapFromScene(scene_rect).boundingRect()
|
|
453
|
+
image_rect = image_rect.intersected(self.pixmap_item.boundingRect().toRect())
|
|
454
|
+
return image_rect
|
|
455
|
+
|
|
456
|
+
def get_visible_image_portion(self):
|
|
457
|
+
if self.has_no_master_layer():
|
|
458
|
+
return None
|
|
459
|
+
visible_rect = self.get_visible_image_region()
|
|
460
|
+
if not visible_rect:
|
|
461
|
+
return self.master_layer()
|
|
462
|
+
x, y = int(visible_rect.x()), int(visible_rect.y())
|
|
463
|
+
w, h = int(visible_rect.width()), int(visible_rect.height())
|
|
464
|
+
master_img = self.master_layer()
|
|
465
|
+
return master_img[y:y + h, x:x + w], (x, y, w, h)
|