shinestacker 0.5.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 +37 -16
- 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 +3 -4
- 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 +275 -371
- 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 +2 -5
- shinestacker/retouch/denoise_filter.py +5 -44
- 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 +318 -40
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +34 -30
- 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.5.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 -266
- shinestacker/retouch/image_editor.py +0 -197
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.5.0.dist-info/RECORD +0 -87
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,43 +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):
|
|
28
|
+
QMainWindow.__init__(self)
|
|
29
|
+
LayerCollectionHandler.__init__(self, LayerCollection())
|
|
26
30
|
self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
|
|
27
|
-
|
|
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
|
|
28
37
|
self.brush = Brush()
|
|
29
|
-
self.
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
self.
|
|
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
|
|
33
47
|
|
|
34
|
-
def setup_shortcuts(self):
|
|
35
|
-
prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
|
|
36
|
-
prev_layer.activated.connect(self.prev_layer)
|
|
37
|
-
next_layer = QShortcut(QKeySequence(Qt.Key_Down), self, context=Qt.ApplicationShortcut)
|
|
38
|
-
next_layer.activated.connect(self.next_layer)
|
|
39
|
-
|
|
40
|
-
def setup_ui(self):
|
|
41
48
|
self.update_title()
|
|
42
49
|
self.resize(1400, 900)
|
|
43
50
|
center = QGuiApplication.primaryScreen().geometry().center()
|
|
@@ -70,6 +77,16 @@ class ImageEditorUI(ImageFilters):
|
|
|
70
77
|
|
|
71
78
|
self.brush_size_slider = QSlider(Qt.Horizontal)
|
|
72
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
|
+
|
|
73
90
|
self.brush_size_slider.setValue(brush_size_to_slider(self.brush.size))
|
|
74
91
|
brush_layout.addWidget(self.brush_size_slider)
|
|
75
92
|
|
|
@@ -141,6 +158,13 @@ class ImageEditorUI(ImageFilters):
|
|
|
141
158
|
gui_constants.UI_SIZES['thumbnail_width'])
|
|
142
159
|
self.master_thumbnail_label.mousePressEvent = \
|
|
143
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
|
|
144
168
|
master_thumbnail_layout.addWidget(self.master_thumbnail_label)
|
|
145
169
|
side_layout.addWidget(self.master_thumbnail_frame)
|
|
146
170
|
side_layout.addSpacing(10)
|
|
@@ -206,13 +230,25 @@ class ImageEditorUI(ImageFilters):
|
|
|
206
230
|
layout.addWidget(control_panel, 0)
|
|
207
231
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
208
232
|
layout.setSpacing(2)
|
|
209
|
-
|
|
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
|
|
210
251
|
|
|
211
|
-
def highlight_master_thumbnail(self):
|
|
212
|
-
self.master_thumbnail_frame.setStyleSheet(
|
|
213
|
-
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
214
|
-
|
|
215
|
-
def setup_menu(self):
|
|
216
252
|
menubar = self.menuBar()
|
|
217
253
|
file_menu = menubar.addMenu("&File")
|
|
218
254
|
file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
|
|
@@ -227,6 +263,7 @@ class ImageEditorUI(ImageFilters):
|
|
|
227
263
|
self.io_gui_handler.save_master_only = QAction("Save Master &Only", self)
|
|
228
264
|
self.io_gui_handler.save_master_only.setCheckable(True)
|
|
229
265
|
self.io_gui_handler.save_master_only.setChecked(True)
|
|
266
|
+
self.io_gui_handler.save_master_only.triggered.connect(self.save_master_only)
|
|
230
267
|
file_menu.addAction(self.io_gui_handler.save_master_only)
|
|
231
268
|
self.save_actions_set_enabled(False)
|
|
232
269
|
|
|
@@ -295,6 +332,12 @@ class ImageEditorUI(ImageFilters):
|
|
|
295
332
|
view_menu.addAction(view_individual_action)
|
|
296
333
|
view_menu.addSeparator()
|
|
297
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
|
+
|
|
298
341
|
sort_asc_action = QAction("Sort Layers A-Z", self)
|
|
299
342
|
sort_asc_action.triggered.connect(lambda: self.sort_layers('asc'))
|
|
300
343
|
view_menu.addAction(sort_asc_action)
|
|
@@ -342,21 +385,254 @@ class ImageEditorUI(ImageFilters):
|
|
|
342
385
|
white_balance_action = QAction("White Balance", self)
|
|
343
386
|
white_balance_action.triggered.connect(self.white_balance)
|
|
344
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)
|
|
345
391
|
|
|
346
392
|
help_menu = menubar.addMenu("&Help")
|
|
347
393
|
help_menu.setObjectName("Help")
|
|
348
394
|
shortcuts_help_action = QAction("Shortcuts and mouse", self)
|
|
349
|
-
|
|
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)
|
|
350
401
|
help_menu.addAction(shortcuts_help_action)
|
|
351
402
|
|
|
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
|
+
|
|
352
626
|
def save_actions_set_enabled(self, enabled):
|
|
353
627
|
self.save_action.setEnabled(enabled)
|
|
354
628
|
self.save_as_action.setEnabled(enabled)
|
|
355
629
|
self.io_gui_handler.save_master_only.setEnabled(enabled)
|
|
356
630
|
|
|
357
631
|
def close_file(self):
|
|
358
|
-
self.
|
|
359
|
-
|
|
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)
|
|
360
636
|
|
|
361
637
|
def set_view_master(self):
|
|
362
638
|
self.display_manager.set_view_master()
|
|
@@ -368,9 +644,11 @@ class ImageEditorUI(ImageFilters):
|
|
|
368
644
|
self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
|
|
369
645
|
self.highlight_master_thumbnail()
|
|
370
646
|
|
|
371
|
-
def
|
|
372
|
-
self.
|
|
373
|
-
|
|
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()
|
|
374
652
|
|
|
375
653
|
def toggle_fullscreen(self, checked):
|
|
376
654
|
if checked:
|
|
@@ -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)
|
|
@@ -14,6 +14,8 @@ from .layer_collection import LayerCollectionHandler
|
|
|
14
14
|
class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
15
15
|
status_message_requested = Signal(str)
|
|
16
16
|
update_title_requested = Signal()
|
|
17
|
+
mark_as_modified_requested = Signal(bool)
|
|
18
|
+
change_layer_requested = Signal(int)
|
|
17
19
|
|
|
18
20
|
def __init__(self, layer_collection, undo_manager, parent):
|
|
19
21
|
QObject.__init__(self, parent)
|
|
@@ -54,16 +56,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
54
56
|
self.set_master_layer(master_layer)
|
|
55
57
|
self.undo_manager.reset()
|
|
56
58
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
57
|
-
self.
|
|
58
|
-
|
|
59
|
-
self.image_viewer.reset_zoom()
|
|
60
|
-
self.status_message_requested.emit(f"Loaded: {self.current_file_path()}")
|
|
61
|
-
self.update_title_requested.emit()
|
|
62
|
-
self.current_file_path_master = ''
|
|
63
|
-
self.current_file_path_multi = ''
|
|
64
|
-
self.parent().mark_as_modified()
|
|
65
|
-
self.parent().change_layer(0)
|
|
66
|
-
self.parent().thumbnail_list.setFocus()
|
|
59
|
+
self.finish_loading_setup(stack, None, master_layer,
|
|
60
|
+
f"Loaded: {self.current_file_path()}")
|
|
67
61
|
|
|
68
62
|
def on_file_error(self, error_msg):
|
|
69
63
|
QApplication.restoreOverrideCursor()
|
|
@@ -71,6 +65,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
71
65
|
self.loading_dialog.accept()
|
|
72
66
|
self.loading_dialog.deleteLater()
|
|
73
67
|
QMessageBox.critical(self.parent(), "Error", error_msg)
|
|
68
|
+
self.current_file_path_master = ''
|
|
69
|
+
self.current_file_path_multi = ''
|
|
74
70
|
self.status_message_requested.emit(f"Error loading: {self.current_file_path()}")
|
|
75
71
|
|
|
76
72
|
def on_multilayer_save_success(self):
|
|
@@ -78,7 +74,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
78
74
|
self.saving_timer.stop()
|
|
79
75
|
self.saving_dialog.hide()
|
|
80
76
|
self.saving_dialog.deleteLater()
|
|
81
|
-
self.
|
|
77
|
+
self.mark_as_modified_requested.emit(False)
|
|
82
78
|
self.update_title_requested.emit()
|
|
83
79
|
self.status_message_requested.emit(f"Saved multilayer to: {self.current_file_path_multi}")
|
|
84
80
|
|
|
@@ -140,20 +136,30 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
140
136
|
msg.setText(str(e))
|
|
141
137
|
msg.exec()
|
|
142
138
|
return
|
|
139
|
+
self.finish_loading_setup(stack, labels, master, "Selected frames imported")
|
|
140
|
+
|
|
141
|
+
def finish_loading_setup(self, stack, labels, master, message):
|
|
143
142
|
if self.layer_stack() is None and len(stack) > 0:
|
|
144
143
|
self.set_layer_stack(np.array(stack))
|
|
145
|
-
|
|
144
|
+
if labels is None:
|
|
145
|
+
labels = self.layer_labels()
|
|
146
|
+
else:
|
|
147
|
+
self.set_layer_labels(labels)
|
|
146
148
|
self.set_master_layer(master)
|
|
147
149
|
self.blank_layer = np.zeros(master.shape[:2])
|
|
148
150
|
else:
|
|
151
|
+
if labels is None:
|
|
152
|
+
labels = self.layer_labels()
|
|
149
153
|
for img, label in zip(stack, labels):
|
|
150
154
|
self.add_layer_label(label)
|
|
151
155
|
self.add_layer(img)
|
|
152
|
-
self.parent().mark_as_modified()
|
|
153
|
-
self.parent().change_layer(0)
|
|
154
|
-
self.image_viewer.reset_zoom()
|
|
155
|
-
self.parent().thumbnail_list.setFocus()
|
|
156
156
|
self.display_manager.update_thumbnails()
|
|
157
|
+
self.mark_as_modified_requested.emit(True)
|
|
158
|
+
self.change_layer_requested.emit(0)
|
|
159
|
+
self.image_viewer.setup_brush_cursor()
|
|
160
|
+
self.image_viewer.reset_zoom()
|
|
161
|
+
self.status_message_requested.emit(message)
|
|
162
|
+
self.update_title_requested.emit()
|
|
157
163
|
|
|
158
164
|
def save_file(self):
|
|
159
165
|
if self.save_master_only.isChecked():
|
|
@@ -239,7 +245,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
239
245
|
try:
|
|
240
246
|
self.io_manager.save_master(path)
|
|
241
247
|
self.current_file_path_master = os.path.abspath(path)
|
|
242
|
-
self.
|
|
248
|
+
self.mark_as_modified_requested.emit(False)
|
|
243
249
|
self.update_title_requested.emit()
|
|
244
250
|
self.status_message_requested.emit(f"Saved master layer to: {path}")
|
|
245
251
|
except Exception as e:
|
|
@@ -255,16 +261,14 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
255
261
|
self.exif_dialog.exec()
|
|
256
262
|
|
|
257
263
|
def close_file(self):
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
self.update_title_requested.emit()
|
|
270
|
-
self.status_message_requested.emit("File closed")
|
|
264
|
+
self.mark_as_modified_requested.emit(False)
|
|
265
|
+
self.blank_layer = None
|
|
266
|
+
self.layer_collection.reset()
|
|
267
|
+
self.current_file_path_master = ''
|
|
268
|
+
self.current_file_path_multi = ''
|
|
269
|
+
self.undo_manager.reset()
|
|
270
|
+
self.image_viewer.clear_image()
|
|
271
|
+
self.display_manager.thumbnail_list.clear()
|
|
272
|
+
self.display_manager.update_thumbnails()
|
|
273
|
+
self.update_title_requested.emit()
|
|
274
|
+
self.status_message_requested.emit("File closed")
|
|
@@ -32,6 +32,7 @@ class ShortcutsHelp(QDialog):
|
|
|
32
32
|
self.create_form(left_layout, right_layout)
|
|
33
33
|
button_box = QHBoxLayout()
|
|
34
34
|
ok_button = QPushButton("OK")
|
|
35
|
+
ok_button.setFixedWidth(100)
|
|
35
36
|
ok_button.setFocus()
|
|
36
37
|
button_box.addWidget(ok_button)
|
|
37
38
|
self.layout.addLayout(button_box)
|
|
@@ -48,6 +49,7 @@ class ShortcutsHelp(QDialog):
|
|
|
48
49
|
shortcuts = {
|
|
49
50
|
"M": "show master layer",
|
|
50
51
|
"L": "show selected layer",
|
|
52
|
+
"T": "toggle master/selected layer",
|
|
51
53
|
"X": "temp. toggle between master and source layer",
|
|
52
54
|
"↑": "select one layer up",
|
|
53
55
|
"↓": "selcet one layer down",
|
|
@@ -80,3 +82,13 @@ class ShortcutsHelp(QDialog):
|
|
|
80
82
|
self.add_bold_label(right_layout, "Mouse Controls")
|
|
81
83
|
for k, v in mouse_controls.items():
|
|
82
84
|
right_layout.addRow(f"<b>{k}</b>", QLabel(v))
|
|
85
|
+
|
|
86
|
+
touchpad_controls = {
|
|
87
|
+
"Two fingers": "pan",
|
|
88
|
+
"Pinch": "zoom in/out",
|
|
89
|
+
"Ctrl + two fingers": "zoom in/out",
|
|
90
|
+
}
|
|
91
|
+
self.add_bold_label(right_layout, " ")
|
|
92
|
+
self.add_bold_label(right_layout, "Touchpad Controls")
|
|
93
|
+
for k, v in touchpad_controls.items():
|
|
94
|
+
right_layout.addRow(f"<b>{k}</b>", QLabel(v))
|