shinestacker 1.3.1__py3-none-any.whl → 1.4.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.

Files changed (34) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +198 -18
  3. shinestacker/algorithms/align_parallel.py +17 -1
  4. shinestacker/algorithms/balance.py +23 -13
  5. shinestacker/algorithms/noise_detection.py +3 -1
  6. shinestacker/algorithms/utils.py +21 -10
  7. shinestacker/algorithms/vignetting.py +2 -0
  8. shinestacker/config/gui_constants.py +2 -2
  9. shinestacker/core/core_utils.py +10 -1
  10. shinestacker/gui/action_config.py +172 -7
  11. shinestacker/gui/action_config_dialog.py +246 -285
  12. shinestacker/gui/gui_run.py +2 -2
  13. shinestacker/gui/main_window.py +14 -5
  14. shinestacker/gui/menu_manager.py +26 -2
  15. shinestacker/gui/project_controller.py +4 -0
  16. shinestacker/gui/recent_file_manager.py +93 -0
  17. shinestacker/retouch/base_filter.py +5 -5
  18. shinestacker/retouch/brush_preview.py +3 -0
  19. shinestacker/retouch/brush_tool.py +11 -11
  20. shinestacker/retouch/display_manager.py +21 -37
  21. shinestacker/retouch/image_editor_ui.py +129 -71
  22. shinestacker/retouch/image_view_status.py +61 -0
  23. shinestacker/retouch/image_viewer.py +89 -431
  24. shinestacker/retouch/io_gui_handler.py +12 -2
  25. shinestacker/retouch/overlaid_view.py +212 -0
  26. shinestacker/retouch/shortcuts_help.py +13 -3
  27. shinestacker/retouch/sidebyside_view.py +479 -0
  28. shinestacker/retouch/view_strategy.py +466 -0
  29. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/METADATA +1 -1
  30. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/RECORD +34 -29
  31. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/WHEEL +0 -0
  32. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/entry_points.txt +0 -0
  33. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/licenses/LICENSE +0 -0
  34. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904
2
+ from functools import partial
2
3
  import numpy as np
3
- from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel,
4
+ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QMenu,
4
5
  QListWidget, QSlider, QMainWindow, QMessageBox)
5
6
  from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
6
7
  from PySide6.QtCore import Qt
@@ -21,12 +22,14 @@ from .denoise_filter import DenoiseFilter
21
22
  from .unsharp_mask_filter import UnsharpMaskFilter
22
23
  from .white_balance_filter import WhiteBalanceFilter
23
24
  from .vignetting_filter import VignettingFilter
25
+ from .. gui.recent_file_manager import RecentFileManager
24
26
 
25
27
 
26
28
  class ImageEditorUI(QMainWindow, LayerCollectionHandler):
27
29
  def __init__(self):
28
30
  QMainWindow.__init__(self)
29
31
  LayerCollectionHandler.__init__(self, LayerCollection())
32
+ self._recent_file_manager = RecentFileManager("shinestacker-recent-images-files.txt")
30
33
  self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
31
34
  self.undo_manager = UndoManager()
32
35
  self.undo_action = None
@@ -53,12 +56,12 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
53
56
  self.setCentralWidget(central_widget)
54
57
  layout = QHBoxLayout(central_widget)
55
58
  self.image_viewer = ImageViewer(self.layer_collection)
56
- self.image_viewer.temp_view_requested.connect(self.handle_temp_view)
57
- self.image_viewer.brush_operation_started.connect(self.begin_copy_brush_area)
58
- self.image_viewer.brush_operation_continued.connect(self.continue_copy_brush_area)
59
- self.image_viewer.brush_operation_ended.connect(self.end_copy_brush_area)
60
- self.image_viewer.brush_size_change_requested.connect(self.handle_brush_size_change)
61
- self.image_viewer.setFocusPolicy(Qt.StrongFocus)
59
+ self.image_viewer.connect_signals(
60
+ self.handle_temp_view,
61
+ self.begin_copy_brush_area,
62
+ self.continue_copy_brush_area,
63
+ self.end_copy_brush_area,
64
+ self.handle_brush_size_change)
62
65
  side_panel = QWidget()
63
66
  side_layout = QVBoxLayout(side_panel)
64
67
  side_layout.setContentsMargins(0, 0, 0, 0)
@@ -115,9 +118,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
115
118
  brush_layout.addWidget(self.flow_slider)
116
119
 
117
120
  side_layout.addWidget(brush_panel)
118
- self.brush_preview = QLabel()
119
- self.brush_preview.setContentsMargins(0, 0, 0, 0)
120
- self.brush_preview.setStyleSheet("""
121
+ self.brush_preview_widget = QLabel()
122
+ self.brush_preview_widget.setContentsMargins(0, 0, 0, 0)
123
+ self.brush_preview_widget.setStyleSheet("""
121
124
  QLabel {
122
125
  background-color: #f0f0f0;
123
126
  border: 1px solid #ccc;
@@ -126,9 +129,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
126
129
  margin: 0px;
127
130
  }
128
131
  """)
129
- self.brush_preview.setAlignment(Qt.AlignCenter)
130
- self.brush_preview.setFixedHeight(100)
131
- brush_layout.addWidget(self.brush_preview)
132
+ self.brush_preview_widget.setAlignment(Qt.AlignCenter)
133
+ self.brush_preview_widget.setFixedHeight(100)
134
+ brush_layout.addWidget(self.brush_preview_widget)
132
135
  side_layout.addWidget(brush_panel)
133
136
 
134
137
  master_label = QLabel("Master")
@@ -236,22 +239,27 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
236
239
  self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
237
240
  self.display_manager.status_message_requested.connect(self.show_status_message)
238
241
  self.display_manager.cursor_preview_state_changed.connect(
239
- lambda state: setattr(self.image_viewer, 'allow_cursor_preview', state))
242
+ self.image_viewer.set_allow_cursor_preview)
240
243
  self.io_gui_handler.status_message_requested.connect(self.show_status_message)
241
244
  self.io_gui_handler.update_title_requested.connect(self.update_title)
242
245
  self.io_gui_handler.mark_as_modified_requested.connect(self.mark_as_modified)
243
246
  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,
247
+ self.io_gui_handler.add_recent_file_requested.connect(self.add_recent_file)
248
+ self.brush_tool.setup_ui(self.brush, self.brush_preview_widget, self.image_viewer,
245
249
  self.brush_size_slider, self.hardness_slider, self.opacity_slider,
246
250
  self.flow_slider)
247
- self.image_viewer.brush = self.brush_tool.brush
251
+ self.image_viewer.set_brush(self.brush_tool.brush)
252
+ self.image_viewer.set_preview_brush(self.brush_tool.brush)
248
253
  self.brush_tool.update_brush_thumb()
249
254
  self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
250
- self.image_viewer.display_manager = self.display_manager
255
+ self.image_viewer.set_display_manager(self.display_manager)
251
256
 
252
257
  menubar = self.menuBar()
253
258
  file_menu = menubar.addMenu("&File")
254
259
  file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
260
+ self.recent_files_menu = QMenu("Open &Recent", file_menu)
261
+ file_menu.addMenu(self.recent_files_menu)
262
+ self.update_recent_files()
255
263
  self.save_action = QAction("&Save", self)
256
264
  self.save_action.setShortcut("Ctrl+S")
257
265
  self.save_action.triggered.connect(self.io_gui_handler.save_file)
@@ -300,6 +308,69 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
300
308
 
301
309
  view_menu.addSeparator()
302
310
 
311
+ view_strategy_menu = QMenu("View &Mode", view_menu)
312
+
313
+ self.view_action_modes = {
314
+ 'overlaid': QAction("Overlaid", self),
315
+ 'sidebyside': QAction("Side By Side", self),
316
+ 'topbottom': QAction("Top-Bottom", self)
317
+ }
318
+ overlaid_mode = self.view_action_modes['overlaid']
319
+ overlaid_mode.setShortcut("Ctrl+1")
320
+ overlaid_mode.setCheckable(True)
321
+ overlaid_mode.triggered.connect(lambda: set_strategy('overlaid'))
322
+ view_strategy_menu.addAction(overlaid_mode)
323
+ side_by_side_mode = self.view_action_modes['sidebyside']
324
+ side_by_side_mode.setShortcut("Ctrl+2")
325
+ side_by_side_mode.setCheckable(True)
326
+ side_by_side_mode.triggered.connect(lambda: set_strategy('sidebyside'))
327
+ view_strategy_menu.addAction(side_by_side_mode)
328
+ side_by_side_mode = self.view_action_modes['topbottom']
329
+ side_by_side_mode.setShortcut("Ctrl+3")
330
+ side_by_side_mode.setCheckable(True)
331
+ side_by_side_mode.triggered.connect(lambda: set_strategy('topbottom'))
332
+ view_strategy_menu.addAction(side_by_side_mode)
333
+ view_menu.addMenu(view_strategy_menu)
334
+
335
+ def set_strategy(strategy):
336
+ self.image_viewer.set_strategy(strategy)
337
+ enable_shortcuts = strategy == 'overlaid'
338
+ self.view_master_action.setEnabled(enable_shortcuts)
339
+ self.view_individual_action.setEnabled(enable_shortcuts)
340
+ self.toggle_view_master_individual_action.setEnabled(enable_shortcuts)
341
+ for label, mode in self.view_action_modes.items():
342
+ mode.setEnabled(label != strategy)
343
+ mode.setChecked(label == strategy)
344
+
345
+ cursor_menu = view_menu.addMenu("Cursor Style")
346
+
347
+ cursor_stype = self.image_viewer.get_cursor_style()
348
+ brush_action = QAction("Simple Brush", self)
349
+ brush_action.setCheckable(True)
350
+ brush_action.setChecked(cursor_stype == 'brush')
351
+ brush_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('brush'))
352
+ cursor_menu.addAction(brush_action)
353
+
354
+ preview_action = QAction("Brush Preview", self)
355
+ preview_action.setCheckable(True)
356
+ preview_action.setChecked(cursor_stype == 'preview')
357
+ preview_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('preview'))
358
+ cursor_menu.addAction(preview_action)
359
+
360
+ outline_action = QAction("Outline Only", self)
361
+ outline_action.setCheckable(True)
362
+ outline_action.setChecked(cursor_stype == 'outline')
363
+ outline_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('outline'))
364
+ cursor_menu.addAction(outline_action)
365
+
366
+ cursor_group = QActionGroup(self)
367
+ cursor_group.addAction(preview_action)
368
+ cursor_group.addAction(outline_action)
369
+ cursor_group.addAction(brush_action)
370
+ cursor_group.setExclusive(True)
371
+
372
+ view_menu.addSeparator()
373
+
303
374
  zoom_in_action = QAction("Zoom In", self)
304
375
  zoom_in_action.setShortcut("Ctrl++")
305
376
  zoom_in_action.triggered.connect(self.image_viewer.zoom_in)
@@ -316,27 +387,30 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
316
387
  view_menu.addAction(adapt_action)
317
388
 
318
389
  actual_size_action = QAction("Actual Size", self)
319
- actual_size_action.setShortcut("Ctrl+=")
390
+ actual_size_action.setShortcut("Ctrl+R")
320
391
  actual_size_action.triggered.connect(self.image_viewer.actual_size)
321
392
  view_menu.addAction(actual_size_action)
322
393
  view_menu.addSeparator()
323
394
 
324
- view_master_action = QAction("View Master", self)
325
- view_master_action.setShortcut("M")
326
- view_master_action.triggered.connect(self.set_view_master)
327
- view_menu.addAction(view_master_action)
328
-
329
- view_individual_action = QAction("View Individual", self)
330
- view_individual_action.setShortcut("L")
331
- view_individual_action.triggered.connect(self.set_view_individual)
332
- view_menu.addAction(view_individual_action)
333
-
334
- toggle_view_master_individual_action = QAction("Toggle Master/Individual", self)
335
- toggle_view_master_individual_action.setShortcut("T")
336
- toggle_view_master_individual_action.triggered.connect(self.toggle_view_master_individual)
337
- view_menu.addAction(toggle_view_master_individual_action)
395
+ self.view_master_action = QAction("View Master", self)
396
+ self.view_master_action.setShortcut("M")
397
+ self.view_master_action.triggered.connect(self.set_view_master)
398
+ view_menu.addAction(self.view_master_action)
399
+
400
+ self.view_individual_action = QAction("View Individual", self)
401
+ self.view_individual_action.setShortcut("L")
402
+ self.view_individual_action.triggered.connect(self.set_view_individual)
403
+ view_menu.addAction(self.view_individual_action)
404
+
405
+ self.toggle_view_master_individual_action = QAction("Toggle Master/Individual", self)
406
+ self.toggle_view_master_individual_action.setShortcut("T")
407
+ self.toggle_view_master_individual_action.triggered.connect(
408
+ self.toggle_view_master_individual)
409
+ view_menu.addAction(self.toggle_view_master_individual_action)
338
410
  view_menu.addSeparator()
339
411
 
412
+ set_strategy('overlaid')
413
+
340
414
  sort_asc_action = QAction("Sort Layers A-Z", self)
341
415
  sort_asc_action.triggered.connect(lambda: self.sort_layers('asc'))
342
416
  view_menu.addAction(sort_asc_action)
@@ -347,32 +421,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
347
421
 
348
422
  view_menu.addSeparator()
349
423
 
350
- cursor_menu = view_menu.addMenu("Cursor Style")
351
-
352
- brush_action = QAction("Simple Brush", self)
353
- brush_action.setCheckable(True)
354
- brush_action.setChecked(self.image_viewer.cursor_style == 'brush')
355
- brush_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('brush'))
356
- cursor_menu.addAction(brush_action)
357
-
358
- preview_action = QAction("Brush Preview", self)
359
- preview_action.setCheckable(True)
360
- preview_action.setChecked(self.image_viewer.cursor_style == 'preview')
361
- preview_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('preview'))
362
- cursor_menu.addAction(preview_action)
363
-
364
- outline_action = QAction("Outline Only", self)
365
- outline_action.setCheckable(True)
366
- outline_action.setChecked(self.image_viewer.cursor_style == 'outline')
367
- outline_action.triggered.connect(lambda: self.image_viewer.set_cursor_style('outline'))
368
- cursor_menu.addAction(outline_action)
369
-
370
- cursor_group = QActionGroup(self)
371
- cursor_group.addAction(preview_action)
372
- cursor_group.addAction(outline_action)
373
- cursor_group.addAction(brush_action)
374
- cursor_group.setExclusive(True)
375
-
376
424
  filter_menu = menubar.addMenu("&Filter")
377
425
  filter_menu.setObjectName("Filter")
378
426
  denoise_action = QAction("Denoise", self)
@@ -415,6 +463,19 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
415
463
  title += " *"
416
464
  self.window().setWindowTitle(title)
417
465
 
466
+ def update_recent_files(self):
467
+ self.recent_files_menu.clear()
468
+ recent_files = self._recent_file_manager.get_files_with_display_names()
469
+ for file_path, display_name in recent_files.items():
470
+ action = self.recent_files_menu.addAction(display_name)
471
+ action.setData(file_path)
472
+ action.triggered.connect(partial(self.io_gui_handler.open_file, file_path))
473
+ self.recent_files_menu.setEnabled(len(recent_files) > 0)
474
+
475
+ def add_recent_file(self, file_path):
476
+ self._recent_file_manager.add_file(file_path)
477
+ self.update_recent_files()
478
+
418
479
  def show_status_message(self, message):
419
480
  self.statusBar().showMessage(message)
420
481
 
@@ -440,7 +501,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
440
501
 
441
502
  # pylint: disable=C0103
442
503
  def keyPressEvent(self, event):
443
- if self.image_viewer.empty:
504
+ if self.image_viewer.empty():
444
505
  return
445
506
  if event.text() == '[':
446
507
  self.brush_tool.decrease_brush_size()
@@ -467,14 +528,12 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
467
528
 
468
529
  def change_layer(self, layer_idx):
469
530
  if 0 <= layer_idx < self.number_of_layers():
470
- view_state = self.image_viewer.get_view_state()
471
531
  self.set_current_layer_idx(layer_idx)
472
- self.display_manager.display_current_view()
473
- self.image_viewer.set_view_state(view_state)
532
+ self.display_manager.refresh_current_view()
474
533
  self.thumbnail_list.setCurrentRow(layer_idx)
475
534
  self.thumbnail_list.setFocus()
476
535
  self.image_viewer.update_brush_cursor()
477
- self.image_viewer.setFocus()
536
+ self.image_viewer.strategy.setFocus()
478
537
 
479
538
  def prev_layer(self):
480
539
  if self.layer_stack() is not None:
@@ -503,8 +562,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
503
562
  if reply == QMessageBox.Yes:
504
563
  self.set_master_layer(self.current_layer().copy())
505
564
  self.master_layer().setflags(write=True)
506
- self.display_manager.display_current_view()
507
- self.display_manager.update_thumbnails()
565
+ self.display_manager.refresh_master_view()
508
566
  self.mark_as_modified()
509
567
  self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
510
568
 
@@ -540,8 +598,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
540
598
 
541
599
  def end_copy_brush_area(self):
542
600
  if self.display_manager.update_timer.isActive():
543
- self.display_manager.display_master_layer()
544
- self.display_manager.update_master_thumbnail()
601
+ self.display_manager.refresh_master_view()
545
602
  self.undo_manager.save_undo_state(self.master_layer_copy(), 'Brush Stroke')
546
603
  self.display_manager.update_timer.stop()
547
604
  self.mark_as_modified()
@@ -629,17 +686,20 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
629
686
 
630
687
  def close_file(self):
631
688
  if self.check_unsaved_changes():
689
+ self.image_viewer.reset_zoom()
632
690
  self.io_gui_handler.close_file()
633
691
  self.set_master_layer(None)
634
692
  self.mark_as_modified(False)
635
693
 
636
694
  def set_view_master(self):
637
695
  self.display_manager.set_view_master()
696
+ self.display_manager.refresh_master_view()
638
697
  self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
639
698
  self.highlight_master_thumbnail()
640
699
 
641
700
  def set_view_individual(self):
642
701
  self.display_manager.set_view_individual()
702
+ self.display_manager.refresh_current_view()
643
703
  self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
644
704
  self.highlight_master_thumbnail()
645
705
 
@@ -663,15 +723,13 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
663
723
 
664
724
  def undo(self):
665
725
  if self.undo_manager.undo(self.master_layer()):
666
- self.display_manager.display_current_view()
667
- self.display_manager.update_master_thumbnail()
726
+ self.display_manager.refresh_master_view()
668
727
  self.mark_as_modified()
669
728
  self.statusBar().showMessage("Undo applied", 2000)
670
729
 
671
730
  def redo(self):
672
731
  if self.undo_manager.redo(self.master_layer()):
673
- self.display_manager.display_current_view()
674
- self.display_manager.update_master_thumbnail()
732
+ self.display_manager.refresh_master_view()
675
733
  self.mark_as_modified()
676
734
  self.statusBar().showMessage("Redo applied", 2000)
677
735
 
@@ -0,0 +1,61 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902
2
+ from PySide6.QtCore import QObject, QRectF
3
+ from PySide6.QtGui import QPixmap
4
+
5
+
6
+ class ImageViewStatus(QObject):
7
+ def __init__(self, parent=None):
8
+ super().__init__(parent)
9
+ self.pixmap_master = QPixmap()
10
+ self.pixmap_current = QPixmap()
11
+ self.zoom_factor = 1.0
12
+ self.min_scale = 0.0
13
+ self.max_scale = 0.0
14
+ self.h_scroll = 0
15
+ self.v_scroll = 0
16
+ self.scene_rect = QRectF()
17
+
18
+ def empty(self):
19
+ return self.pixmap_master.isNull()
20
+
21
+ def set_master_image(self, qimage):
22
+ pixmap = QPixmap.fromImage(qimage)
23
+ self.pixmap_master = pixmap
24
+ if not self.empty():
25
+ self.scene_rect = QRectF(pixmap.rect())
26
+
27
+ def set_current_image(self, qimage):
28
+ pixmap = QPixmap.fromImage(qimage)
29
+ self.pixmap_current = pixmap
30
+
31
+ def clear(self):
32
+ self.pixmap_master = QPixmap()
33
+ self.pixmap_current = QPixmap()
34
+ self.zoom_factor = 1.0
35
+ self.min_scale = 0.0
36
+ self.max_scale = 0.0
37
+ self.h_scroll = 0
38
+ self.v_scroll = 0
39
+ self.scene_rect = QRectF()
40
+
41
+ def get_state(self):
42
+ return {
43
+ 'zoom': self.zoom_factor,
44
+ 'h_scroll': self.h_scroll,
45
+ 'v_scroll': self.v_scroll
46
+ }
47
+
48
+ def set_state(self, state):
49
+ if state:
50
+ self.zoom_factor = state['zoom']
51
+ self.h_scroll = state['h_scroll']
52
+ self.v_scroll = state['v_scroll']
53
+
54
+ def set_zoom_factor(self, zoom_factor):
55
+ self.zoom_factor = zoom_factor
56
+
57
+ def set_min_scale(self, min_scale):
58
+ self.min_scale = min_scale
59
+
60
+ def set_max_scale(self, min_scale):
61
+ self.max_scale = min_scale