shinestacker 1.3.1__py3-none-any.whl → 1.5.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 (38) 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/app/main.py +1 -1
  9. shinestacker/config/gui_constants.py +7 -2
  10. shinestacker/core/core_utils.py +10 -1
  11. shinestacker/gui/action_config.py +172 -7
  12. shinestacker/gui/action_config_dialog.py +246 -285
  13. shinestacker/gui/gui_run.py +2 -2
  14. shinestacker/gui/main_window.py +14 -5
  15. shinestacker/gui/menu_manager.py +26 -2
  16. shinestacker/gui/project_controller.py +4 -0
  17. shinestacker/gui/recent_file_manager.py +93 -0
  18. shinestacker/retouch/base_filter.py +13 -15
  19. shinestacker/retouch/brush_preview.py +3 -1
  20. shinestacker/retouch/brush_tool.py +11 -11
  21. shinestacker/retouch/display_manager.py +43 -59
  22. shinestacker/retouch/image_editor_ui.py +161 -82
  23. shinestacker/retouch/image_view_status.py +65 -0
  24. shinestacker/retouch/image_viewer.py +95 -431
  25. shinestacker/retouch/io_gui_handler.py +12 -2
  26. shinestacker/retouch/layer_collection.py +3 -0
  27. shinestacker/retouch/overlaid_view.py +215 -0
  28. shinestacker/retouch/shortcuts_help.py +13 -3
  29. shinestacker/retouch/sidebyside_view.py +477 -0
  30. shinestacker/retouch/transformation_manager.py +43 -0
  31. shinestacker/retouch/undo_manager.py +22 -3
  32. shinestacker/retouch/view_strategy.py +557 -0
  33. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/METADATA +7 -7
  34. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/RECORD +38 -32
  35. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/WHEEL +0 -0
  36. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/entry_points.txt +0 -0
  37. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/licenses/LICENSE +0 -0
  38. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,7 @@ from PySide6.QtCore import Signal, Slot
9
9
  from .. config.constants import constants
10
10
  from .. config.gui_constants import gui_constants
11
11
  from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
12
- from .. algorithms.utils import extension_tif_jpg, extension_pdf
12
+ from .. algorithms.utils import extension_jpg_tif_png, extension_pdf
13
13
  from .gui_logging import LogWorker, QTextEditLogger
14
14
  from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
15
15
  from .colors import (
@@ -209,7 +209,7 @@ class RunWindow(QTextEditLogger):
209
209
  try:
210
210
  if extension_pdf(path):
211
211
  image_view = GuiPdfView(path, self)
212
- elif extension_tif_jpg(path):
212
+ elif extension_jpg_tif_png(path):
213
213
  image_view = GuiImageView(path, self)
214
214
  else:
215
215
  raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
@@ -134,19 +134,28 @@ class MainWindow(QMainWindow, LogManager):
134
134
  self.update_title()
135
135
 
136
136
  self.project_editor.modified_signal.connect(handle_modified)
137
- self.project_editor.select_signal.connect(self.update_delete_action_state)
138
- self.project_editor.refresh_ui_signal.connect(self.refresh_ui)
137
+ self.project_editor.select_signal.connect(
138
+ self.update_delete_action_state)
139
+ self.project_editor.refresh_ui_signal.connect(
140
+ self.refresh_ui)
139
141
  self.project_editor.enable_delete_action_signal.connect(
140
142
  self.menu_manager.delete_element_action.setEnabled)
141
143
  self.project_editor.undo_manager.set_enabled_undo_action_requested.connect(
142
144
  self.menu_manager.set_enabled_undo_action)
143
- self.project_controller.update_title_requested.connect(self.update_title)
144
- self.project_controller.refresh_ui_requested.connect(self.refresh_ui)
145
- self.project_controller.activate_window_requested.connect(self.activateWindow)
145
+ self.project_controller.update_title_requested.connect(
146
+ self.update_title)
147
+ self.project_controller.refresh_ui_requested.connect(
148
+ self.refresh_ui)
149
+ self.project_controller.activate_window_requested.connect(
150
+ self.activateWindow)
146
151
  self.project_controller.enable_save_actions_requested.connect(
147
152
  self.menu_manager.save_actions_set_enabled)
148
153
  self.project_controller.enable_sub_actions_requested.connect(
149
154
  self.menu_manager.set_enabled_sub_actions_gui)
155
+ self.project_controller.add_recent_file_requested.connect(
156
+ self.menu_manager.add_recent_file)
157
+ self.menu_manager.open_file_requested.connect(
158
+ self.project_controller.open_project)
150
159
 
151
160
  def modified(self):
152
161
  return self.project_editor.modified()
@@ -1,13 +1,20 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201
2
2
  import os
3
+ from functools import partial
4
+ from PySide6.QtCore import Signal, QObject
3
5
  from PySide6.QtGui import QAction, QIcon
4
6
  from PySide6.QtWidgets import QMenu, QComboBox
5
7
  from .. config.constants import constants
8
+ from .recent_file_manager import RecentFileManager
6
9
 
7
10
 
8
- class MenuManager:
11
+ class MenuManager(QObject):
12
+ open_file_requested = Signal(str)
13
+
9
14
  def __init__(self, menubar, actions, project_editor, parent):
15
+ super().__init__(parent)
10
16
  self.script_dir = os.path.dirname(__file__)
17
+ self._recent_file_manager = RecentFileManager("shinestacker-recent-project-files.txt")
11
18
  self.project_editor = project_editor
12
19
  self.parent = parent
13
20
  self.menubar = menubar
@@ -69,10 +76,27 @@ class MenuManager:
69
76
  action.triggered.connect(action_fun)
70
77
  return action
71
78
 
79
+ def update_recent_files(self):
80
+ self.recent_files_menu.clear()
81
+ recent_files = self._recent_file_manager.get_files_with_display_names()
82
+ for file_path, display_name in recent_files.items():
83
+ action = self.recent_files_menu.addAction(display_name)
84
+ action.setData(file_path)
85
+ action.triggered.connect(partial(self.open_file_requested.emit, file_path))
86
+ self.recent_files_menu.setEnabled(len(recent_files) > 0)
87
+
88
+ def add_recent_file(self, file_path):
89
+ self._recent_file_manager.add_file(file_path)
90
+ self.update_recent_files()
91
+
72
92
  def add_file_menu(self):
73
93
  menu = self.menubar.addMenu("&File")
74
- for name in ["&New...", "&Open...", "&Close"]:
94
+ for name in ["&New...", "&Open..."]:
75
95
  menu.addAction(self.action(name))
96
+ self.recent_files_menu = QMenu("Open &Recent", menu)
97
+ menu.addMenu(self.recent_files_menu)
98
+ self.update_recent_files()
99
+ menu.addAction(self.action("&Close"))
76
100
  menu.addSeparator()
77
101
  self.save_action = self.action("&Save")
78
102
  menu.addAction(self.save_action)
@@ -20,6 +20,7 @@ class ProjectController(QObject):
20
20
  activate_window_requested = Signal()
21
21
  enable_save_actions_requested = Signal(bool)
22
22
  enable_sub_actions_requested = Signal(bool)
23
+ add_recent_file_requested = Signal(str)
23
24
 
24
25
  def __init__(self, parent):
25
26
  super().__init__(parent)
@@ -242,6 +243,7 @@ class ProjectController(QObject):
242
243
  self.parent, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
243
244
  if file_path:
244
245
  try:
246
+ abs_file_path = os.path.abspath(file_path)
245
247
  self.set_current_file_path(file_path)
246
248
  with open(self.current_file_path(), 'r', encoding="utf-8") as file:
247
249
  json_obj = json.load(file)
@@ -250,6 +252,7 @@ class ProjectController(QObject):
250
252
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
251
253
  self.set_project(project)
252
254
  self.mark_as_modified(False)
255
+ self.add_recent_file_requested.emit(abs_file_path)
253
256
  self.project_editor.reset_undo()
254
257
  self.refresh_ui(0, -1)
255
258
  if self.job_list_count() > 0:
@@ -311,6 +314,7 @@ class ProjectController(QObject):
311
314
  f.write(json_obj)
312
315
  self.mark_as_modified(False)
313
316
  self.update_title_requested.emit()
317
+ self.add_recent_file_requested.emit(file_path)
314
318
  except Exception as e:
315
319
  QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
316
320
 
@@ -0,0 +1,93 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611
2
+ import os
3
+ from PySide6.QtCore import QStandardPaths
4
+
5
+
6
+ class RecentFileManager:
7
+ def __init__(self, filename, max_entries=10):
8
+ self.filename = filename
9
+ self.max_entries = max_entries
10
+ self._config_dir = None
11
+
12
+ def get_config_dir(self):
13
+ if self._config_dir is None:
14
+ config_dir = QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)
15
+ if not config_dir:
16
+ if os.name == 'nt': # Windows
17
+ config_dir = os.path.join(os.environ.get('APPDATA', ''), 'ShineStacker')
18
+ elif os.name == 'posix': # macOS and Linux
19
+ config_dir = os.path.expanduser('~/.config/shinestacker')
20
+ else:
21
+ config_dir = os.path.join(os.path.expanduser('~'), '.shinestacker')
22
+ os.makedirs(config_dir, exist_ok=True)
23
+ self._config_dir = config_dir
24
+ return self._config_dir
25
+
26
+ def get_file_path(self):
27
+ return os.path.join(self.get_config_dir(), self.filename)
28
+
29
+ def get_files(self):
30
+ file_path = self.get_file_path()
31
+ try:
32
+ with open(file_path, 'r', encoding='utf-8') as f:
33
+ files = [line.strip() for line in f.readlines()]
34
+ return [f for f in files if f and os.path.exists(f)]
35
+ except (FileNotFoundError, IOError):
36
+ return []
37
+
38
+ def get_files_with_display_names(self):
39
+ files = self.get_files()
40
+ basename_count = {}
41
+ for file_path in files:
42
+ basename = os.path.basename(file_path)
43
+ basename_count[basename] = basename_count.get(basename, 0) + 1
44
+ result = {}
45
+ for file_path in files:
46
+ basename = os.path.basename(file_path)
47
+ if basename_count[basename] == 1:
48
+ result[file_path] = basename
49
+ else:
50
+ parent_dir = os.path.basename(os.path.dirname(file_path))
51
+ result[file_path] = f"{basename} ({parent_dir})"
52
+ if list(result.values()).count(result[file_path]) > 1:
53
+ path_components = file_path.split(os.sep)
54
+ for i in range(2, min(5, len(path_components))):
55
+ display_name = os.sep.join(path_components[-i:])
56
+ if sum(1 for f in files
57
+ if os.sep.join(os.path.normpath(f).split(os.sep)[-i:]) ==
58
+ display_name) == 1:
59
+ result[file_path] = display_name
60
+ break
61
+ else:
62
+ if len(file_path) > 50:
63
+ result[file_path] = "..." + file_path[-47:]
64
+ else:
65
+ result[file_path] = file_path
66
+ return result
67
+
68
+ def add_file(self, file_path):
69
+ file_path = os.path.abspath(file_path)
70
+ recent_files = self.get_files()
71
+ if file_path in recent_files:
72
+ recent_files.remove(file_path)
73
+ recent_files.insert(0, file_path)
74
+ recent_files = recent_files[:self.max_entries]
75
+ self._save_files(recent_files)
76
+
77
+ def remove_file(self, file_path):
78
+ file_path = os.path.normpath(file_path)
79
+ recent_files = self.get_files()
80
+ if file_path in recent_files:
81
+ recent_files.remove(file_path)
82
+ self._save_files(recent_files)
83
+
84
+ def _save_files(self, files):
85
+ file_path = self.get_file_path()
86
+ try:
87
+ with open(file_path, 'w', encoding='utf-8') as f:
88
+ f.write('\n'.join(files))
89
+ except IOError as e:
90
+ raise e
91
+
92
+ def clear(self):
93
+ self._save_files([])
@@ -34,7 +34,6 @@ class BaseFilter(ABC):
34
34
  def run_with_preview(self, **kwargs):
35
35
  if self.editor.has_no_master_layer():
36
36
  return
37
-
38
37
  self.editor.copy_master_layer()
39
38
  dlg = QDialog(self.editor)
40
39
  layout = QVBoxLayout(dlg)
@@ -48,7 +47,7 @@ class BaseFilter(ABC):
48
47
  nonlocal active_worker, dialog_closed # noqa
49
48
  dialog_closed = True
50
49
  self.editor.restore_master_layer()
51
- self.editor.display_manager.display_master_layer()
50
+ self.editor.image_viewer.update_master_display()
52
51
  if active_worker and active_worker.isRunning():
53
52
  active_worker.wait()
54
53
  initial_timer.stop()
@@ -62,10 +61,10 @@ class BaseFilter(ABC):
62
61
  current_region = self.editor.image_viewer.get_visible_image_portion()[1]
63
62
  if current_region == region:
64
63
  self.editor.set_master_layer(img)
65
- self.editor.display_manager.display_master_layer()
64
+ self.editor.image_viewer.update_master_display()
66
65
  else:
67
66
  self.editor.set_master_layer(img)
68
- self.editor.display_manager.display_master_layer()
67
+ self.editor.image_viewer.update_master_display()
69
68
  try:
70
69
  dlg.activateWindow()
71
70
  except Exception:
@@ -125,7 +124,7 @@ class BaseFilter(ABC):
125
124
 
126
125
  def restore_original():
127
126
  self.editor.restore_master_layer()
128
- self.editor.display_manager.display_master_layer()
127
+ self.editor.image_viewer.update_master_display()
129
128
  try:
130
129
  dlg.activateWindow()
131
130
  except Exception:
@@ -143,19 +142,18 @@ class BaseFilter(ABC):
143
142
  h, w = self.editor.master_layer().shape[:2]
144
143
  except Exception:
145
144
  h, w = self.editor.master_layer_copy().shape[:2]
146
- if hasattr(self.editor, "undo_manager"):
147
- try:
148
- self.editor.undo_manager.extend_undo_area(0, 0, w, h)
149
- self.editor.undo_manager.save_undo_state(
150
- self.editor.master_layer_copy(),
151
- self.name
152
- )
153
- except Exception:
154
- pass
145
+ try:
146
+ self.editor.undo_manager.extend_undo_area(0, 0, w, h)
147
+ self.editor.undo_manager.save_undo_state(
148
+ self.editor.master_layer_copy(),
149
+ self.name
150
+ )
151
+ except Exception:
152
+ pass
155
153
  final_img = self.apply(self.editor.master_layer_copy(), *params)
156
154
  self.editor.set_master_layer(final_img)
157
155
  self.editor.copy_master_layer()
158
- self.editor.display_manager.display_master_layer()
156
+ self.editor.image_viewer.update_master_display()
159
157
  self.editor.display_manager.update_master_thumbnail()
160
158
  self.editor.mark_as_modified()
161
159
  else:
@@ -41,6 +41,7 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
41
41
  self.setVisible(False)
42
42
  self.setZValue(500)
43
43
  self.setTransformationMode(Qt.SmoothTransformation)
44
+ self.brush = None
44
45
 
45
46
  def get_layer_area(self, layer, x, y, w, h):
46
47
  if not isinstance(layer, np.ndarray):
@@ -64,6 +65,8 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
64
65
  raise RuntimeError("Bitmas is neither 8 bit nor 16, but of type " + area.dtype)
65
66
 
66
67
  def update(self, scene_pos, size):
68
+ if self.brush is None:
69
+ return
67
70
  try:
68
71
  if self.layer_collection is None or self.number_of_layers() == 0 or size <= 0:
69
72
  self.hide()
@@ -110,7 +113,6 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
110
113
  self.setPixmap(final_pixmap)
111
114
  x_start, y_start = max(0, x), max(0, y)
112
115
  self.setPos(x_start, y_start)
113
- self.show()
114
116
  except Exception:
115
117
  traceback.print_exc()
116
118
  self.hide()
@@ -12,7 +12,7 @@ from .brush_preview import create_brush_mask
12
12
  class BrushTool:
13
13
  def __init__(self):
14
14
  self.brush = None
15
- self.brush_preview = None
15
+ self.brush_preview_widget = None
16
16
  self.image_viewer = None
17
17
  self.size_slider = None
18
18
  self.hardness_slider = None
@@ -21,11 +21,11 @@ class BrushTool:
21
21
  self._brush_mask_cache = {}
22
22
  self.brush_text = None
23
23
 
24
- def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
24
+ def setup_ui(self, brush, brush_preview_widget, image_viewer, size_slider, hardness_slider,
25
25
  opacity_slider, flow_slider):
26
26
  self.brush = brush
27
- self.brush_preview = brush_preview
28
- self.brush_text = QLabel(brush_preview.parent())
27
+ self.brush_preview_widget = brush_preview_widget
28
+ self.brush_text = QLabel(brush_preview_widget.parent())
29
29
  self.brush_text.setStyleSheet("color: navy; background: transparent;")
30
30
  self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
31
31
  self.brush_text.raise_()
@@ -96,13 +96,13 @@ class BrushTool:
96
96
  preview_size = min(self.brush.size, width + 30, height + 30)
97
97
  center_x, center_y = width // 2, height // 2
98
98
  radius = preview_size // 2
99
- if self.image_viewer.cursor_style == 'preview':
99
+ if self.image_viewer.strategy.cursor_style == 'preview':
100
100
  gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
101
101
  painter.setBrush(QBrush(gradient))
102
102
  painter.setPen(
103
103
  QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
104
104
  gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
105
- elif self.image_viewer.cursor_style == 'outline':
105
+ elif self.image_viewer.strategy.cursor_style == 'outline':
106
106
  painter.setBrush(Qt.NoBrush)
107
107
  painter.setPen(
108
108
  QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
@@ -113,7 +113,7 @@ class BrushTool:
113
113
  QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
114
114
  gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
115
115
  painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
116
- if self.image_viewer.cursor_style == 'preview':
116
+ if self.image_viewer.strategy.cursor_style == 'preview':
117
117
  painter.setPen(QPen(QColor(0, 0, 160)))
118
118
  font = QApplication.font()
119
119
  painter.setFont(font)
@@ -126,13 +126,13 @@ class BrushTool:
126
126
  f"Flow: {self.brush.flow}%"
127
127
  )
128
128
  self.brush_text.adjustSize()
129
- self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
129
+ self.brush_text.move(10, self.brush_preview_widget.height() // 2 + 125)
130
130
  self.brush_text.show()
131
131
  else:
132
132
  self.brush_text.hide()
133
133
  painter.end()
134
- self.brush_preview.setPixmap(pixmap)
135
- self.image_viewer.update_brush_cursor()
134
+ self.brush_preview_widget.setPixmap(pixmap)
135
+ self.image_viewer.strategy.update_brush_cursor()
136
136
 
137
137
  def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer,
138
138
  view_pos):
@@ -140,7 +140,7 @@ class BrushTool:
140
140
  return False
141
141
  if dest_layer is None:
142
142
  dest_layer = master_layer
143
- scene_pos = self.image_viewer.mapToScene(view_pos)
143
+ scene_pos = self.image_viewer.strategy.map_to_scene(view_pos)
144
144
  x_center = int(round(scene_pos.x()))
145
145
  y_center = int(round(scene_pos.y()))
146
146
  radius = int(round(self.brush.size // 2))
@@ -25,7 +25,6 @@ class ClickableLabel(QLabel):
25
25
 
26
26
  class DisplayManager(QObject, LayerCollectionHandler):
27
27
  status_message_requested = Signal(str)
28
- cursor_preview_state_changed = Signal(bool)
29
28
 
30
29
  def __init__(self, layer_collection, image_viewer, master_thumbnail_label,
31
30
  thumbnail_list, parent=None):
@@ -35,7 +34,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
35
34
  self.master_thumbnail_label = master_thumbnail_label
36
35
  self.thumbnail_list = thumbnail_list
37
36
  self.view_mode = 'master'
38
- self.temp_view_individual = False
39
37
  self.needs_update = False
40
38
  self.update_timer = QTimer()
41
39
  self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
@@ -44,29 +42,13 @@ class DisplayManager(QObject, LayerCollectionHandler):
44
42
 
45
43
  def process_pending_updates(self):
46
44
  if self.needs_update:
47
- self.display_master_layer()
45
+ self.refresh_master_view()
48
46
  self.needs_update = False
49
47
 
50
- def display_image(self, img):
51
- if img is None:
52
- self.image_viewer.clear_image()
53
- else:
54
- self.image_viewer.set_image(self.numpy_to_qimage(img))
55
-
56
- def display_current_layer(self):
57
- self.display_image(self.current_layer())
58
-
59
- def display_master_layer(self):
60
- self.display_image(self.master_layer())
61
-
62
- def display_current_view(self):
63
- if self.temp_view_individual or self.view_mode == 'individual':
64
- self.display_current_layer()
65
- else:
66
- self.display_master_layer()
67
-
68
48
  def create_thumbnail(self, layer):
69
49
  source_layer = (layer // 256).astype(np.uint8) if layer.dtype == np.uint16 else layer
50
+ if not source_layer.flags.c_contiguous:
51
+ source_layer = np.ascontiguousarray(source_layer)
70
52
  height, width = source_layer.shape[:2]
71
53
  if layer.ndim == 3 and source_layer.shape[-1] == 3:
72
54
  qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
@@ -162,59 +144,61 @@ class DisplayManager(QObject, LayerCollectionHandler):
162
144
  self.thumbnail_list.scrollToItem(
163
145
  self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
164
146
 
147
+ def _master_refresh_and_thumb(self):
148
+ self.image_viewer.show_master()
149
+ self.refresh_master_view()
150
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
151
+ self.highlight_thumbnail(self.current_layer_idx())
152
+
153
+ def _current_refresh_and_thumb(self):
154
+ self.image_viewer.show_current()
155
+ self.refresh_current_view()
156
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
157
+ self.highlight_thumbnail(self.current_layer_idx())
158
+
165
159
  def set_view_master(self):
166
160
  if self.has_no_master_layer():
167
161
  return
168
162
  self.view_mode = 'master'
169
- self.temp_view_individual = False
170
- self.display_master_layer()
171
- self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
172
- self.highlight_thumbnail(self.current_layer_idx())
163
+ self._master_refresh_and_thumb()
173
164
  self.status_message_requested.emit("View mode: Master")
174
- self.cursor_preview_state_changed.emit(True) # True = allow preview
175
165
 
176
166
  def set_view_individual(self):
177
167
  if self.has_no_master_layer():
178
168
  return
179
169
  self.view_mode = 'individual'
180
- self.temp_view_individual = False
181
- self.display_current_layer()
182
- self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
183
- self.highlight_thumbnail(self.current_layer_idx())
170
+ self._current_refresh_and_thumb()
184
171
  self.status_message_requested.emit("View mode: Individual layers")
185
- self.cursor_preview_state_changed.emit(False) # False = no preview
172
+
173
+ def refresh_master_view(self):
174
+ if self.has_no_master_layer():
175
+ return
176
+ self.image_viewer.update_master_display()
177
+ self.image_viewer.refresh_display()
178
+ self.update_master_thumbnail()
179
+
180
+ def refresh_current_view(self):
181
+ if self.number_of_layers() == 0:
182
+ return
183
+ self.image_viewer.update_current_display()
184
+ self.image_viewer.refresh_display()
186
185
 
187
186
  def start_temp_view(self):
188
- if not self.temp_view_individual and self.view_mode == 'master':
189
- self.temp_view_individual = True
190
- self.image_viewer.update_brush_cursor()
191
- self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
192
- self.highlight_thumbnail(self.current_layer_idx())
193
- self.display_current_layer()
194
- self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
187
+ if self.view_mode == 'master':
188
+ self._current_refresh_and_thumb()
189
+ self.status_message_requested.emit("Temporary view: Individual layer")
190
+ else:
191
+ self._master_refresh_and_thumb()
192
+ self.image_viewer.strategy.brush_preview.hide()
193
+ self.status_message_requested.emit("Temporary view: Master")
195
194
 
196
195
  def end_temp_view(self):
197
- if self.temp_view_individual:
198
- self.temp_view_individual = False
199
- self.image_viewer.update_brush_cursor()
200
- self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
201
- self.highlight_thumbnail(self.current_layer_idx())
202
- self.display_master_layer()
196
+ if self.view_mode == 'master':
197
+ self._master_refresh_and_thumb()
203
198
  self.status_message_requested.emit("View mode: Master")
204
- self.cursor_preview_state_changed.emit(True) # Restore preview
205
-
206
- def numpy_to_qimage(self, array):
207
- if array.dtype == np.uint16:
208
- array = np.right_shift(array, 8).astype(np.uint8)
209
- if array.ndim == 2:
210
- height, width = array.shape
211
- return QImage(memoryview(array), width, height, width, QImage.Format_Grayscale8)
212
- if array.ndim == 3:
213
- height, width, _ = array.shape
214
- if not array.flags['C_CONTIGUOUS']:
215
- array = np.ascontiguousarray(array)
216
- return QImage(memoryview(array), width, height, 3 * width, QImage.Format_RGB888)
217
- return QImage()
199
+ else:
200
+ self._current_refresh_and_thumb()
201
+ self.status_message_requested.emit("View: Individual layer")
218
202
 
219
203
  def allow_cursor_preview(self):
220
- return self.view_mode == 'master' and not self.temp_view_individual
204
+ return self.view_mode == 'master'