shinestacker 0.3.6__py3-none-any.whl → 0.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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +37 -20
- shinestacker/algorithms/balance.py +2 -1
- shinestacker/algorithms/base_stack_algo.py +2 -1
- shinestacker/algorithms/multilayer.py +11 -8
- shinestacker/algorithms/noise_detection.py +13 -7
- shinestacker/algorithms/stack.py +5 -4
- shinestacker/algorithms/stack_framework.py +12 -10
- shinestacker/app/about_dialog.py +69 -1
- shinestacker/app/main.py +1 -1
- shinestacker/config/config.py +1 -0
- shinestacker/config/constants.py +8 -1
- shinestacker/config/gui_constants.py +7 -5
- shinestacker/core/framework.py +15 -10
- shinestacker/gui/action_config.py +11 -7
- shinestacker/gui/actions_window.py +8 -0
- shinestacker/gui/gui_logging.py +8 -7
- shinestacker/gui/gui_run.py +8 -8
- shinestacker/gui/main_window.py +17 -12
- shinestacker/gui/new_project.py +31 -17
- shinestacker/gui/project_converter.py +0 -1
- shinestacker/gui/select_path_widget.py +3 -1
- shinestacker/retouch/brush_tool.py +23 -6
- shinestacker/retouch/display_manager.py +57 -20
- shinestacker/retouch/image_editor.py +5 -9
- shinestacker/retouch/image_editor_ui.py +55 -16
- shinestacker/retouch/image_viewer.py +104 -20
- shinestacker/retouch/io_gui_handler.py +74 -24
- shinestacker/retouch/io_manager.py +23 -8
- shinestacker/retouch/layer_collection.py +2 -1
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/METADATA +5 -4
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/RECORD +36 -36
- shinestacker-0.5.0.dist-info/licenses/LICENSE +165 -0
- shinestacker-0.3.6.dist-info/licenses/LICENSE +0 -1
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -30,6 +30,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
30
30
|
def mark_as_modified(self):
|
|
31
31
|
self._modified_project = True
|
|
32
32
|
self.project_buffer.append(self.project.clone())
|
|
33
|
+
self.save_actions_set_enabled(True)
|
|
33
34
|
self.update_title()
|
|
34
35
|
|
|
35
36
|
def close_project(self):
|
|
@@ -40,6 +41,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
40
41
|
self.job_list.clear()
|
|
41
42
|
self.action_list.clear()
|
|
42
43
|
self._modified_project = False
|
|
44
|
+
self.save_actions_set_enabled(False)
|
|
43
45
|
|
|
44
46
|
def new_project(self):
|
|
45
47
|
if not self._check_unsaved_changes():
|
|
@@ -51,8 +53,10 @@ class ActionsWindow(ProjectEditor):
|
|
|
51
53
|
self.job_list.clear()
|
|
52
54
|
self.action_list.clear()
|
|
53
55
|
self.set_project(Project())
|
|
56
|
+
self.save_actions_set_enabled(False)
|
|
54
57
|
dialog = NewProjectDialog(self)
|
|
55
58
|
if dialog.exec() == QDialog.Accepted:
|
|
59
|
+
self.save_actions_set_enabled(True)
|
|
56
60
|
input_folder = dialog.get_input_folder().split('/')
|
|
57
61
|
working_path = '/'.join(input_folder[:-1])
|
|
58
62
|
input_path = input_folder[-1]
|
|
@@ -142,6 +146,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
142
146
|
if len(self.project.jobs) > 0:
|
|
143
147
|
self.job_list.setCurrentRow(0)
|
|
144
148
|
self.activateWindow()
|
|
149
|
+
self.save_actions_set_enabled(True)
|
|
145
150
|
for job in self.project.jobs:
|
|
146
151
|
if 'working_path' in job.params.keys():
|
|
147
152
|
working_path = job.params['working_path']
|
|
@@ -256,3 +261,6 @@ class ActionsWindow(ProjectEditor):
|
|
|
256
261
|
if dialog.exec() == QDialog.Accepted:
|
|
257
262
|
self.on_job_selected(self.job_list.currentRow())
|
|
258
263
|
self.mark_as_modified()
|
|
264
|
+
|
|
265
|
+
def save_actions_set_enabled(self, enabled):
|
|
266
|
+
pass
|
shinestacker/gui/gui_logging.py
CHANGED
|
@@ -17,6 +17,7 @@ class SimpleHtmlFormatter(logging.Formatter):
|
|
|
17
17
|
FF = '80'
|
|
18
18
|
OO = '00'
|
|
19
19
|
MM = '40'
|
|
20
|
+
GG = 'FF'
|
|
20
21
|
ANSI_COLORS = {
|
|
21
22
|
# Reset
|
|
22
23
|
'\x1b[0m': '</span>',
|
|
@@ -32,13 +33,13 @@ class SimpleHtmlFormatter(logging.Formatter):
|
|
|
32
33
|
'\x1b[37m': f'<span style="color:#{FF}{FF}{FF}">', # white
|
|
33
34
|
# Brilliant colors (90-97)
|
|
34
35
|
'\x1b[90m': f'<span style="color:#{MM}{MM}{MM}">',
|
|
35
|
-
'\x1b[91m': f'<span style="color:#{
|
|
36
|
-
'\x1b[92m': f'<span style="color:#{MM}{
|
|
37
|
-
'\x1b[93m': f'<span style="color:#{
|
|
38
|
-
'\x1b[94m': f'<span style="color:#{MM}{MM}{
|
|
39
|
-
'\x1b[95m': f'<span style="color:#{
|
|
40
|
-
'\x1b[96m': f'<span style="color:#{MM}{
|
|
41
|
-
'\x1b[97m': f'<span style="color:#{
|
|
36
|
+
'\x1b[91m': f'<span style="color:#{GG}{MM}{MM}">',
|
|
37
|
+
'\x1b[92m': f'<span style="color:#{MM}{GG}{MM}">',
|
|
38
|
+
'\x1b[93m': f'<span style="color:#{GG}{GG}{MM}">',
|
|
39
|
+
'\x1b[94m': f'<span style="color:#{MM}{MM}{GG}">',
|
|
40
|
+
'\x1b[95m': f'<span style="color:#{GG}{MM}{GG}">',
|
|
41
|
+
'\x1b[96m': f'<span style="color:#{MM}{GG}{GG}">',
|
|
42
|
+
'\x1b[97m': f'<span style="color:#{GG}{GG}{GG}">',
|
|
42
43
|
# Background (40-47)
|
|
43
44
|
'\x1b[40m': f'<span style="background-color:#{OO}{OO}{OO}">',
|
|
44
45
|
'\x1b[41m': f'<span style="background-color:#{FF}{OO}{OO}">',
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -13,6 +13,10 @@ from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
|
|
|
13
13
|
from .colors import ColorPalette
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
ACTION_RUNNING_COLOR = ColorPalette.MEDIUM_BLUE
|
|
17
|
+
ACTION_DONE_COLOR = ColorPalette.MEDIUM_GREEN
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
class ColorButton(QPushButton):
|
|
17
21
|
def __init__(self, text, enabled, parent=None):
|
|
18
22
|
super().__init__(text.replace(gui_constants.DISABLED_TAG, ''), parent)
|
|
@@ -36,10 +40,6 @@ class ColorButton(QPushButton):
|
|
|
36
40
|
""")
|
|
37
41
|
|
|
38
42
|
|
|
39
|
-
action_running_color = ColorPalette.MEDIUM_BLUE
|
|
40
|
-
action_done_color = ColorPalette.MEDIUM_GREEN
|
|
41
|
-
|
|
42
|
-
|
|
43
43
|
class TimerProgressBar(QProgressBar):
|
|
44
44
|
light_background_color = ColorPalette.LIGHT_BLUE
|
|
45
45
|
border_color = ColorPalette.DARK_BLUE
|
|
@@ -123,10 +123,10 @@ class TimerProgressBar(QProgressBar):
|
|
|
123
123
|
# pylint: enable=C0103
|
|
124
124
|
|
|
125
125
|
def set_running_style(self):
|
|
126
|
-
self.set_style(
|
|
126
|
+
self.set_style(ACTION_RUNNING_COLOR)
|
|
127
127
|
|
|
128
128
|
def set_done_style(self):
|
|
129
|
-
self.set_style(
|
|
129
|
+
self.set_style(ACTION_DONE_COLOR)
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
class RunWindow(QTextEditLogger):
|
|
@@ -246,7 +246,7 @@ class RunWindow(QTextEditLogger):
|
|
|
246
246
|
@Slot(int, str)
|
|
247
247
|
def handle_before_action(self, run_id, _name):
|
|
248
248
|
if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
|
|
249
|
-
self.color_widgets[self.row_widget_id][run_id].set_color(*
|
|
249
|
+
self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_RUNNING_COLOR.tuple())
|
|
250
250
|
self.progress_bar.start(1)
|
|
251
251
|
if run_id == -1:
|
|
252
252
|
self.progress_bar.set_running_style()
|
|
@@ -254,7 +254,7 @@ class RunWindow(QTextEditLogger):
|
|
|
254
254
|
@Slot(int, str)
|
|
255
255
|
def handle_after_action(self, run_id, _name):
|
|
256
256
|
if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
|
|
257
|
-
self.color_widgets[self.row_widget_id][run_id].set_color(*
|
|
257
|
+
self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_DONE_COLOR.tuple())
|
|
258
258
|
self.progress_bar.stop()
|
|
259
259
|
if run_id == -1:
|
|
260
260
|
self.row_widget_id += 1
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -163,11 +163,11 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
163
163
|
self.action_list.itemDoubleClicked.connect(self.on_action_edit)
|
|
164
164
|
vbox_left = QVBoxLayout()
|
|
165
165
|
vbox_left.setSpacing(4)
|
|
166
|
-
vbox_left.addWidget(QLabel("
|
|
166
|
+
vbox_left.addWidget(QLabel("Job"))
|
|
167
167
|
vbox_left.addWidget(self.job_list)
|
|
168
168
|
vbox_right = QVBoxLayout()
|
|
169
169
|
vbox_right.setSpacing(4)
|
|
170
|
-
vbox_right.addWidget(QLabel("
|
|
170
|
+
vbox_right.addWidget(QLabel("Action"))
|
|
171
171
|
vbox_right.addWidget(self.action_list)
|
|
172
172
|
self.job_list.itemSelectionChanged.connect(self.update_delete_action_state)
|
|
173
173
|
self.action_list.itemSelectionChanged.connect(self.update_delete_action_state)
|
|
@@ -189,19 +189,24 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
189
189
|
open_action.setShortcut("Ctrl+O")
|
|
190
190
|
open_action.triggered.connect(self.open_project)
|
|
191
191
|
menu.addAction(open_action)
|
|
192
|
-
save_action = QAction("&Save", self)
|
|
193
|
-
save_action.setShortcut("Ctrl+S")
|
|
194
|
-
save_action.triggered.connect(self.save_project)
|
|
195
|
-
menu.addAction(save_action)
|
|
196
|
-
save_as_action = QAction("Save &As...", self)
|
|
197
|
-
save_as_action.setShortcut("Ctrl+Shift+S")
|
|
198
|
-
save_as_action.triggered.connect(self.save_project_as)
|
|
199
|
-
menu.addAction(save_as_action)
|
|
192
|
+
self.save_action = QAction("&Save", self)
|
|
193
|
+
self.save_action.setShortcut("Ctrl+S")
|
|
194
|
+
self.save_action.triggered.connect(self.save_project)
|
|
195
|
+
menu.addAction(self.save_action)
|
|
196
|
+
self.save_as_action = QAction("Save &As...", self)
|
|
197
|
+
self.save_as_action.setShortcut("Ctrl+Shift+S")
|
|
198
|
+
self.save_as_action.triggered.connect(self.save_project_as)
|
|
199
|
+
menu.addAction(self.save_as_action)
|
|
200
|
+
self.save_actions_set_enabled(False)
|
|
200
201
|
close_action = QAction("&Close", self)
|
|
201
202
|
close_action.setShortcut("Ctrl+W")
|
|
202
203
|
close_action.triggered.connect(self.close_project)
|
|
203
204
|
menu.addAction(close_action)
|
|
204
205
|
|
|
206
|
+
def save_actions_set_enabled(self, enabled):
|
|
207
|
+
self.save_action.setEnabled(enabled)
|
|
208
|
+
self.save_as_action.setEnabled(enabled)
|
|
209
|
+
|
|
205
210
|
def add_edit_menu(self, menubar):
|
|
206
211
|
menu = menubar.addMenu("&Edit")
|
|
207
212
|
undo_action = QAction("&Undo", self)
|
|
@@ -615,7 +620,7 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
615
620
|
labels = [[(self.action_text(a), a.enabled()) for a in job.sub_actions]]
|
|
616
621
|
r = self.get_retouch_path(job)
|
|
617
622
|
retouch_paths = [] if len(r) == 0 else [(job_name, r)]
|
|
618
|
-
new_window, id_str = self.create_new_window("Job
|
|
623
|
+
new_window, id_str = self.create_new_window(f"{job_name} [⚙️ Job]",
|
|
619
624
|
labels, retouch_paths)
|
|
620
625
|
worker = JobLogWorker(job, id_str)
|
|
621
626
|
self.connect_signals(worker, new_window)
|
|
@@ -638,7 +643,7 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
638
643
|
r = self.get_retouch_path(job)
|
|
639
644
|
if len(r) > 0:
|
|
640
645
|
retouch_paths.append((job.params["name"], r))
|
|
641
|
-
new_window, id_str = self.create_new_window("Project
|
|
646
|
+
new_window, id_str = self.create_new_window(f"{project_name} [Project 📚]",
|
|
642
647
|
labels, retouch_paths)
|
|
643
648
|
worker = ProjectLogWorker(self.project, id_str)
|
|
644
649
|
self.connect_signals(worker, new_window)
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -9,6 +9,8 @@ from .. config.constants import constants
|
|
|
9
9
|
from .. algorithms.stack import get_bunches
|
|
10
10
|
from .select_path_widget import create_select_file_paths_widget
|
|
11
11
|
|
|
12
|
+
DEFAULT_NO_COUNT_LABEL = " - "
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
class NewProjectDialog(QDialog):
|
|
14
16
|
def __init__(self, parent=None):
|
|
@@ -51,8 +53,9 @@ class NewProjectDialog(QDialog):
|
|
|
51
53
|
spacer.setFixedHeight(10)
|
|
52
54
|
self.layout.addRow(spacer)
|
|
53
55
|
|
|
54
|
-
container = create_select_file_paths_widget(
|
|
55
|
-
|
|
56
|
+
self.input_folder, container = create_select_file_paths_widget(
|
|
57
|
+
'', 'input files folder', 'input files folder')
|
|
58
|
+
self.input_folder.textChanged.connect(self.update_bunches_label)
|
|
56
59
|
self.noise_detection = QCheckBox()
|
|
57
60
|
self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
|
|
58
61
|
self.vignetting_correction = QCheckBox()
|
|
@@ -72,7 +75,8 @@ class NewProjectDialog(QDialog):
|
|
|
72
75
|
bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
|
|
73
76
|
self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
|
|
74
77
|
self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
|
|
75
|
-
self.bunches_label = QLabel(
|
|
78
|
+
self.bunches_label = QLabel(DEFAULT_NO_COUNT_LABEL)
|
|
79
|
+
self.frames_label = QLabel(DEFAULT_NO_COUNT_LABEL)
|
|
76
80
|
|
|
77
81
|
self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
|
|
78
82
|
self.bunch_stack.toggled.connect(self.update_bunch_options)
|
|
@@ -88,6 +92,7 @@ class NewProjectDialog(QDialog):
|
|
|
88
92
|
|
|
89
93
|
self.add_bold_label("Select input:")
|
|
90
94
|
self.layout.addRow("Input folder:", container)
|
|
95
|
+
self.layout.addRow("Number of frames: ", self.frames_label)
|
|
91
96
|
self.add_bold_label("Select actions:")
|
|
92
97
|
if self.expert():
|
|
93
98
|
self.layout.addRow("Automatic noise detection:", self.noise_detection)
|
|
@@ -111,25 +116,34 @@ class NewProjectDialog(QDialog):
|
|
|
111
116
|
self.update_bunches_label()
|
|
112
117
|
|
|
113
118
|
def update_bunches_label(self):
|
|
119
|
+
if not self.input_folder.text():
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def count_image_files(path):
|
|
123
|
+
if path == '' or not os.path.isdir(path):
|
|
124
|
+
return 0
|
|
125
|
+
extensions = ['jpg', 'jpeg', 'tif', 'tiff']
|
|
126
|
+
count = 0
|
|
127
|
+
for filename in os.listdir(path):
|
|
128
|
+
if '.' in filename:
|
|
129
|
+
ext = filename.lower().split('.')[-1]
|
|
130
|
+
if ext in extensions:
|
|
131
|
+
count += 1
|
|
132
|
+
return count
|
|
133
|
+
|
|
134
|
+
n_image_files = count_image_files(self.input_folder.text())
|
|
135
|
+
if n_image_files == 0:
|
|
136
|
+
self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
137
|
+
self.frames_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
138
|
+
return
|
|
139
|
+
self.frames_label.setText(f"{n_image_files}")
|
|
114
140
|
if self.bunch_stack.isChecked():
|
|
115
|
-
|
|
116
|
-
if path == '' or not os.path.isdir(path):
|
|
117
|
-
return 0
|
|
118
|
-
extensions = ['jpg', 'jpeg', 'tif', 'tiff']
|
|
119
|
-
count = 0
|
|
120
|
-
for filename in os.listdir(path):
|
|
121
|
-
if '.' in filename:
|
|
122
|
-
ext = filename.lower().split('.')[-1]
|
|
123
|
-
if ext in extensions:
|
|
124
|
-
count += 1
|
|
125
|
-
return count
|
|
126
|
-
|
|
127
|
-
bunches = get_bunches(list(range(count_image_files(self.input_folder.text()))),
|
|
141
|
+
bunches = get_bunches(list(range(n_image_files)),
|
|
128
142
|
self.bunch_frames.value(),
|
|
129
143
|
self.bunch_overlap.value())
|
|
130
144
|
self.bunches_label.setText(f"{len(bunches)}")
|
|
131
145
|
else:
|
|
132
|
-
self.bunches_label.setText(
|
|
146
|
+
self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
133
147
|
|
|
134
148
|
def accept(self):
|
|
135
149
|
input_folder = self.input_folder.text()
|
|
@@ -112,7 +112,6 @@ class ProjectConverter:
|
|
|
112
112
|
else:
|
|
113
113
|
raise InvalidOptionError('stacker', stacker, f"valid options are: "
|
|
114
114
|
f"{constants.STACK_ALGO_PYRAMID}, "
|
|
115
|
-
f"{constants.STACK_ALGO_PYRAMID_BLOCK}, "
|
|
116
115
|
f"{constants.STACK_ALGO_DEPTH_MAP}")
|
|
117
116
|
if action_config.type_name == constants.ACTION_FOCUSSTACK:
|
|
118
117
|
return FocusStack(**module_dict, stack_algo=stack_algo)
|
|
@@ -9,6 +9,7 @@ def create_layout_widget_no_margins(layout):
|
|
|
9
9
|
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
10
10
|
return container
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
def create_layout_widget_and_connect(button, edit, browse):
|
|
13
14
|
button.clicked.connect(browse)
|
|
14
15
|
button.setAutoDefault(False)
|
|
@@ -17,6 +18,7 @@ def create_layout_widget_and_connect(button, edit, browse):
|
|
|
17
18
|
layout.addWidget(button)
|
|
18
19
|
return create_layout_widget_no_margins(layout)
|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
def create_select_file_paths_widget(value, placeholder, tag):
|
|
21
23
|
edit = QLineEdit(value)
|
|
22
24
|
edit.setPlaceholderText(placeholder)
|
|
@@ -27,4 +29,4 @@ def create_select_file_paths_widget(value, placeholder, tag):
|
|
|
27
29
|
if path:
|
|
28
30
|
edit.setText(path)
|
|
29
31
|
|
|
30
|
-
return create_layout_widget_and_connect(button, edit, browse)
|
|
32
|
+
return edit, create_layout_widget_and_connect(button, edit, browse)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
|
|
2
2
|
import numpy as np
|
|
3
|
-
from PySide6.
|
|
3
|
+
from PySide6.QtWidgets import QApplication, QLabel
|
|
4
|
+
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QFont
|
|
4
5
|
from PySide6.QtCore import Qt, QPoint
|
|
5
6
|
from .brush_gradient import create_default_brush_gradient
|
|
6
7
|
from .. config.gui_constants import gui_constants
|
|
@@ -18,11 +19,16 @@ class BrushTool:
|
|
|
18
19
|
self.opacity_slider = None
|
|
19
20
|
self.flow_slider = None
|
|
20
21
|
self._brush_mask_cache = {}
|
|
22
|
+
self.brush_text = None
|
|
21
23
|
|
|
22
24
|
def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
|
|
23
25
|
opacity_slider, flow_slider):
|
|
24
26
|
self.brush = brush
|
|
25
27
|
self.brush_preview = brush_preview
|
|
28
|
+
self.brush_text = QLabel(brush_preview.parent())
|
|
29
|
+
self.brush_text.setStyleSheet("color: navy; background: transparent;")
|
|
30
|
+
self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
31
|
+
self.brush_text.raise_()
|
|
26
32
|
self.image_viewer = image_viewer
|
|
27
33
|
self.size_slider = size_slider
|
|
28
34
|
self.hardness_slider = hardness_slider
|
|
@@ -86,7 +92,7 @@ class BrushTool:
|
|
|
86
92
|
pixmap = QPixmap(width, height)
|
|
87
93
|
pixmap.fill(Qt.transparent)
|
|
88
94
|
painter = QPainter(pixmap)
|
|
89
|
-
painter.setRenderHint(QPainter.
|
|
95
|
+
painter.setRenderHint(QPainter.TextAntialiasing, True)
|
|
90
96
|
preview_size = min(self.brush.size, width + 30, height + 30)
|
|
91
97
|
center_x, center_y = width // 2, height // 2
|
|
92
98
|
radius = preview_size // 2
|
|
@@ -109,10 +115,21 @@ class BrushTool:
|
|
|
109
115
|
painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
|
|
110
116
|
if self.image_viewer.cursor_style == 'preview':
|
|
111
117
|
painter.setPen(QPen(QColor(0, 0, 160)))
|
|
112
|
-
|
|
113
|
-
painter.
|
|
114
|
-
|
|
115
|
-
painter.
|
|
118
|
+
font = QApplication.font()
|
|
119
|
+
painter.setFont(font)
|
|
120
|
+
font.setHintingPreference(QFont.PreferFullHinting)
|
|
121
|
+
painter.setFont(font)
|
|
122
|
+
self.brush_text.setText(
|
|
123
|
+
f"Size: {int(self.brush.size)}px\n"
|
|
124
|
+
f"Hardness: {self.brush.hardness}%\n"
|
|
125
|
+
f"Opacity: {self.brush.opacity}%\n"
|
|
126
|
+
f"Flow: {self.brush.flow}%"
|
|
127
|
+
)
|
|
128
|
+
self.brush_text.adjustSize()
|
|
129
|
+
self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
|
|
130
|
+
self.brush_text.show()
|
|
131
|
+
else:
|
|
132
|
+
self.brush_text.hide()
|
|
116
133
|
painter.end()
|
|
117
134
|
self.brush_preview.setPixmap(pixmap)
|
|
118
135
|
self.image_viewer.update_brush_cursor()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902
|
|
2
2
|
import numpy as np
|
|
3
|
-
from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
|
|
3
|
+
from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
|
|
4
|
+
QAbstractItemView)
|
|
4
5
|
from PySide6.QtGui import QPixmap, QImage
|
|
5
6
|
from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
|
|
6
7
|
from .. config.gui_constants import gui_constants
|
|
@@ -39,6 +40,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
39
40
|
self.update_timer = QTimer()
|
|
40
41
|
self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
|
|
41
42
|
self.update_timer.timeout.connect(self.process_pending_updates)
|
|
43
|
+
self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
|
|
42
44
|
|
|
43
45
|
def process_pending_updates(self):
|
|
44
46
|
if self.needs_update:
|
|
@@ -64,15 +66,15 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
64
66
|
self.display_master_layer()
|
|
65
67
|
|
|
66
68
|
def create_thumbnail(self, layer):
|
|
67
|
-
if layer.dtype == np.uint16
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
69
|
+
source_layer = (layer // 256).astype(np.uint8) if layer.dtype == np.uint16 else layer
|
|
70
|
+
height, width = source_layer.shape[:2]
|
|
71
|
+
if layer.ndim == 3 and source_layer.shape[-1] == 3:
|
|
72
|
+
qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
72
73
|
else:
|
|
73
|
-
qimg = QImage(
|
|
74
|
+
qimg = QImage(source_layer.data, width, height, width, QImage.Format_Grayscale8)
|
|
74
75
|
return QPixmap.fromImage(
|
|
75
|
-
qimg.
|
|
76
|
+
qimg.scaledToWidth(
|
|
77
|
+
gui_constants.UI_SIZES['thumbnail_width'], Qt.SmoothTransformation))
|
|
76
78
|
|
|
77
79
|
def update_thumbnails(self):
|
|
78
80
|
self.update_master_thumbnail()
|
|
@@ -103,17 +105,22 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
103
105
|
self.master_thumbnail_label.setPixmap(pixmap)
|
|
104
106
|
|
|
105
107
|
def add_thumbnail_item(self, thumbnail, label, i, is_current):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
container = QWidget()
|
|
109
|
+
container.setFixedWidth(gui_constants.UI_SIZES['thumbnail_width'] + 4)
|
|
110
|
+
container.setObjectName("thumbnailContainer")
|
|
111
|
+
container_layout = QVBoxLayout(container)
|
|
112
|
+
container_layout.setContentsMargins(2, 2, 2, 2)
|
|
113
|
+
container_layout.setSpacing(0)
|
|
114
|
+
content_widget = QWidget()
|
|
115
|
+
content_layout = QVBoxLayout(content_widget)
|
|
116
|
+
content_layout.setContentsMargins(0, 0, 0, 0)
|
|
117
|
+
content_layout.setSpacing(0)
|
|
111
118
|
thumbnail_label = QLabel()
|
|
112
119
|
thumbnail_label.setPixmap(thumbnail)
|
|
113
120
|
thumbnail_label.setAlignment(Qt.AlignCenter)
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
content_layout.addWidget(thumbnail_label)
|
|
116
122
|
label_widget = ClickableLabel(label)
|
|
123
|
+
label_widget.setFixedHeight(gui_constants.UI_SIZES['label_height'])
|
|
117
124
|
label_widget.setAlignment(Qt.AlignCenter)
|
|
118
125
|
|
|
119
126
|
def rename_label(label_widget, old_label, i):
|
|
@@ -124,21 +131,45 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
124
131
|
self.set_layer_labels(i, new_label)
|
|
125
132
|
|
|
126
133
|
label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
|
|
127
|
-
|
|
134
|
+
content_layout.addWidget(label_widget)
|
|
135
|
+
container_layout.addWidget(content_widget)
|
|
136
|
+
if is_current:
|
|
137
|
+
container.setStyleSheet(
|
|
138
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
139
|
+
else:
|
|
140
|
+
container.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
|
|
128
141
|
item = QListWidgetItem()
|
|
129
|
-
item.setSizeHint(QSize(gui_constants.
|
|
142
|
+
item.setSizeHint(QSize(gui_constants.UI_SIZES['thumbnail_width'] + 4,
|
|
143
|
+
thumbnail.height() + label_widget.height() + 4))
|
|
130
144
|
self.thumbnail_list.addItem(item)
|
|
131
|
-
self.thumbnail_list.setItemWidget(item,
|
|
132
|
-
|
|
145
|
+
self.thumbnail_list.setItemWidget(item, container)
|
|
133
146
|
if is_current:
|
|
134
147
|
self.thumbnail_list.setCurrentItem(item)
|
|
135
148
|
|
|
149
|
+
def highlight_thumbnail(self, index):
|
|
150
|
+
for i in range(self.thumbnail_list.count()):
|
|
151
|
+
item = self.thumbnail_list.item(i)
|
|
152
|
+
widget = self.thumbnail_list.itemWidget(item)
|
|
153
|
+
if widget:
|
|
154
|
+
widget.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
|
|
155
|
+
current_item = self.thumbnail_list.item(index)
|
|
156
|
+
if current_item:
|
|
157
|
+
widget = self.thumbnail_list.itemWidget(current_item)
|
|
158
|
+
if widget:
|
|
159
|
+
widget.setStyleSheet(
|
|
160
|
+
f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
|
|
161
|
+
self.thumbnail_list.setCurrentRow(index)
|
|
162
|
+
self.thumbnail_list.scrollToItem(
|
|
163
|
+
self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
164
|
+
|
|
136
165
|
def set_view_master(self):
|
|
137
166
|
if self.has_no_master_layer():
|
|
138
167
|
return
|
|
139
168
|
self.view_mode = 'master'
|
|
140
169
|
self.temp_view_individual = False
|
|
141
170
|
self.display_master_layer()
|
|
171
|
+
self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
|
|
172
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
142
173
|
self.status_message_requested.emit("View mode: Master")
|
|
143
174
|
self.cursor_preview_state_changed.emit(True) # True = allow preview
|
|
144
175
|
|
|
@@ -148,6 +179,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
148
179
|
self.view_mode = 'individual'
|
|
149
180
|
self.temp_view_individual = False
|
|
150
181
|
self.display_current_layer()
|
|
182
|
+
self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
|
|
183
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
151
184
|
self.status_message_requested.emit("View mode: Individual layers")
|
|
152
185
|
self.cursor_preview_state_changed.emit(False) # False = no preview
|
|
153
186
|
|
|
@@ -155,6 +188,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
155
188
|
if not self.temp_view_individual and self.view_mode == 'master':
|
|
156
189
|
self.temp_view_individual = True
|
|
157
190
|
self.image_viewer.update_brush_cursor()
|
|
191
|
+
self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
|
|
192
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
158
193
|
self.display_current_layer()
|
|
159
194
|
self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
|
|
160
195
|
|
|
@@ -162,6 +197,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
162
197
|
if self.temp_view_individual:
|
|
163
198
|
self.temp_view_individual = False
|
|
164
199
|
self.image_viewer.update_brush_cursor()
|
|
200
|
+
self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
|
|
201
|
+
self.highlight_thumbnail(self.current_layer_idx())
|
|
165
202
|
self.display_master_layer()
|
|
166
203
|
self.status_message_requested.emit("View mode: Master")
|
|
167
204
|
self.cursor_preview_state_changed.emit(True) # Restore preview
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0902
|
|
2
|
-
from PySide6.QtWidgets import QMainWindow, QMessageBox
|
|
2
|
+
from PySide6.QtWidgets import QMainWindow, QMessageBox
|
|
3
3
|
from .. config.constants import constants
|
|
4
4
|
from .undo_manager import UndoManager
|
|
5
5
|
from .layer_collection import LayerCollection
|
|
@@ -87,7 +87,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
|
87
87
|
def update_title(self):
|
|
88
88
|
title = constants.APP_TITLE
|
|
89
89
|
if self.io_gui_handler is not None:
|
|
90
|
-
path = self.io_gui_handler.
|
|
90
|
+
path = self.io_gui_handler.current_file_path()
|
|
91
91
|
if path != '':
|
|
92
92
|
title += f" - {path.split('/')[-1]}"
|
|
93
93
|
if self.modified:
|
|
@@ -96,6 +96,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
|
96
96
|
|
|
97
97
|
def mark_as_modified(self):
|
|
98
98
|
self.modified = True
|
|
99
|
+
self.save_actions_set_enabled(True)
|
|
99
100
|
self.update_title()
|
|
100
101
|
|
|
101
102
|
def change_layer(self, layer_idx):
|
|
@@ -114,19 +115,14 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
|
114
115
|
new_idx = max(0, self.current_layer_idx() - 1)
|
|
115
116
|
if new_idx != self.current_layer_idx():
|
|
116
117
|
self.change_layer(new_idx)
|
|
117
|
-
self.highlight_thumbnail(new_idx)
|
|
118
|
+
self.display_manager.highlight_thumbnail(new_idx)
|
|
118
119
|
|
|
119
120
|
def next_layer(self):
|
|
120
121
|
if self.layer_stack() is not None:
|
|
121
122
|
new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
|
|
122
123
|
if new_idx != self.current_layer_idx():
|
|
123
124
|
self.change_layer(new_idx)
|
|
124
|
-
self.highlight_thumbnail(new_idx)
|
|
125
|
-
|
|
126
|
-
def highlight_thumbnail(self, index):
|
|
127
|
-
self.thumbnail_list.setCurrentRow(index)
|
|
128
|
-
self.thumbnail_list.scrollToItem(
|
|
129
|
-
self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
125
|
+
self.display_manager.highlight_thumbnail(new_idx)
|
|
130
126
|
|
|
131
127
|
def copy_layer_to_master(self):
|
|
132
128
|
if self.layer_stack() is None or self.master_layer() is None:
|