shinestacker 0.2.0.post1.dev1__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/__init__.py +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import jsonpickle
|
|
5
|
+
import traceback
|
|
6
|
+
from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
|
|
7
|
+
from .. core.core_utils import get_app_base_path
|
|
8
|
+
from .. config.constants import constants
|
|
9
|
+
from .project_model import ActionConfig
|
|
10
|
+
from .action_config import ActionConfigDialog
|
|
11
|
+
from .project_editor import ProjectEditor
|
|
12
|
+
from .new_project import NewProjectDialog
|
|
13
|
+
from .project_model import Project
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ActionsWindow(ProjectEditor):
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._current_file = None
|
|
20
|
+
self._current_file_wd = ''
|
|
21
|
+
self._modified_project = False
|
|
22
|
+
self.update_title()
|
|
23
|
+
|
|
24
|
+
def update_title(self):
|
|
25
|
+
title = constants.APP_TITLE
|
|
26
|
+
if self._current_file:
|
|
27
|
+
title += f" - {os.path.basename(self._current_file)}"
|
|
28
|
+
if self._modified_project:
|
|
29
|
+
title += " *"
|
|
30
|
+
self.window().setWindowTitle(title)
|
|
31
|
+
|
|
32
|
+
def mark_as_modified(self):
|
|
33
|
+
self._modified_project = True
|
|
34
|
+
self._project_buffer.append(self.project.clone())
|
|
35
|
+
self.update_title()
|
|
36
|
+
|
|
37
|
+
def close_project(self):
|
|
38
|
+
if self._check_unsaved_changes():
|
|
39
|
+
self.set_project(Project())
|
|
40
|
+
self._current_file = None
|
|
41
|
+
self.update_title()
|
|
42
|
+
self.job_list.clear()
|
|
43
|
+
self.action_list.clear()
|
|
44
|
+
self._modified_project = False
|
|
45
|
+
|
|
46
|
+
def new_project(self):
|
|
47
|
+
if not self._check_unsaved_changes():
|
|
48
|
+
return
|
|
49
|
+
os.chdir(get_app_base_path())
|
|
50
|
+
self._current_file = None
|
|
51
|
+
self._modified_project = False
|
|
52
|
+
self.update_title()
|
|
53
|
+
self.job_list.clear()
|
|
54
|
+
self.action_list.clear()
|
|
55
|
+
dialog = NewProjectDialog(self)
|
|
56
|
+
if dialog.exec() == QDialog.Accepted:
|
|
57
|
+
input_folder = dialog.get_input_folder().split('/')
|
|
58
|
+
working_path = '/'.join(input_folder[:-1])
|
|
59
|
+
input_path = input_folder[-1]
|
|
60
|
+
project = Project()
|
|
61
|
+
if dialog.get_noise_detection():
|
|
62
|
+
job_noise = ActionConfig(constants.ACTION_JOB,
|
|
63
|
+
{'name': 'detect-noise', 'working_path': working_path,
|
|
64
|
+
'input_path': input_path})
|
|
65
|
+
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
66
|
+
{'name': 'detect-noise'})
|
|
67
|
+
job_noise.add_sub_action(noise_detection)
|
|
68
|
+
project.jobs.append(job_noise)
|
|
69
|
+
job = ActionConfig(constants.ACTION_JOB,
|
|
70
|
+
{'name': 'focus-stack', 'working_path': working_path,
|
|
71
|
+
'input_path': input_path})
|
|
72
|
+
if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
|
|
73
|
+
dialog.get_align_frames() or dialog.get_balance_frames():
|
|
74
|
+
combo_action = ActionConfig(constants.ACTION_COMBO, {'name': 'align'})
|
|
75
|
+
if dialog.get_noise_detection():
|
|
76
|
+
mask_noise = ActionConfig(constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
|
|
77
|
+
combo_action.add_sub_action(mask_noise)
|
|
78
|
+
if dialog.get_vignetting_correction():
|
|
79
|
+
vignetting = ActionConfig(constants.ACTION_VIGNETTING, {'name': 'vignetting'})
|
|
80
|
+
combo_action.add_sub_action(vignetting)
|
|
81
|
+
if dialog.get_align_frames():
|
|
82
|
+
align = ActionConfig(constants.ACTION_ALIGNFRAMES, {'name': 'align'})
|
|
83
|
+
combo_action.add_sub_action(align)
|
|
84
|
+
if dialog.get_balance_frames():
|
|
85
|
+
balance = ActionConfig(constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
|
|
86
|
+
combo_action.add_sub_action(balance)
|
|
87
|
+
job.add_sub_action(combo_action)
|
|
88
|
+
if dialog.get_bunch_stack():
|
|
89
|
+
bunch_stack = ActionConfig(constants.ACTION_FOCUSSTACKBUNCH,
|
|
90
|
+
{'name': 'bunches', 'frames': dialog.get_bunch_frames(),
|
|
91
|
+
'overlap': dialog.get_bunch_overlap()})
|
|
92
|
+
job.add_sub_action(bunch_stack)
|
|
93
|
+
if dialog.get_focus_stack_pyramid():
|
|
94
|
+
focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
95
|
+
{'name': 'focus-stack-pyramid',
|
|
96
|
+
'stacker': constants.STACK_ALGO_PYRAMID})
|
|
97
|
+
job.add_sub_action(focus_pyramid)
|
|
98
|
+
if dialog.get_focus_stack_depth_map():
|
|
99
|
+
focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
100
|
+
{'name': 'focus-stack-depth-map',
|
|
101
|
+
'stacker': constants.STACK_ALGO_DEPTH_MAP})
|
|
102
|
+
job.add_sub_action(focus_depth_map)
|
|
103
|
+
if dialog.get_multi_layer():
|
|
104
|
+
input_path = []
|
|
105
|
+
if dialog.get_focus_stack_pyramid():
|
|
106
|
+
input_path.append("focus-stack-pyramid")
|
|
107
|
+
if dialog.get_focus_stack_depth_map():
|
|
108
|
+
input_path.append("focus-stack-depth-map")
|
|
109
|
+
if dialog.get_bunch_stack():
|
|
110
|
+
input_path.append("bunches")
|
|
111
|
+
else:
|
|
112
|
+
input_path.append(input_path)
|
|
113
|
+
multi_layer = ActionConfig(constants.ACTION_MULTILAYER,
|
|
114
|
+
{'name': 'multi-layer',
|
|
115
|
+
'input_path': ','.join(input_path)})
|
|
116
|
+
job.add_sub_action(multi_layer)
|
|
117
|
+
project.jobs.append(job)
|
|
118
|
+
self.set_project(project)
|
|
119
|
+
self._modified_project = True
|
|
120
|
+
self.refresh_ui(0, -1)
|
|
121
|
+
|
|
122
|
+
def open_project(self, file_path=False):
|
|
123
|
+
if not self._check_unsaved_changes():
|
|
124
|
+
return
|
|
125
|
+
if file_path is False:
|
|
126
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
|
|
127
|
+
if file_path:
|
|
128
|
+
try:
|
|
129
|
+
self._current_file_wd = '' if os.path.isabs(file_path) else os.getcwd()
|
|
130
|
+
file = open(file_path, 'r')
|
|
131
|
+
pp = file_path.split('/')
|
|
132
|
+
if len(pp) > 1:
|
|
133
|
+
os.chdir('/'.join(pp[:-1]))
|
|
134
|
+
json_obj = json.load(file)
|
|
135
|
+
project = Project.from_dict(json_obj['project'])
|
|
136
|
+
if project is None:
|
|
137
|
+
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
138
|
+
self.set_project(project)
|
|
139
|
+
self._modified_project = False
|
|
140
|
+
self._current_file = file_path
|
|
141
|
+
self.update_title()
|
|
142
|
+
self.refresh_ui(0, -1)
|
|
143
|
+
if self.job_list.count() > 0:
|
|
144
|
+
self.job_list.setCurrentRow(0)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
traceback.print_tb(e.__traceback__)
|
|
147
|
+
QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
|
|
148
|
+
if len(self.project.jobs) > 0:
|
|
149
|
+
self.job_list.setCurrentRow(0)
|
|
150
|
+
self.activateWindow()
|
|
151
|
+
for job in self.project.jobs:
|
|
152
|
+
if 'working_path' in job.params.keys():
|
|
153
|
+
working_path = job.params['working_path']
|
|
154
|
+
if not os.path.isdir(working_path):
|
|
155
|
+
QMessageBox.warning(self, "Working path not found",
|
|
156
|
+
f'''The working path specified in the project file for the job:
|
|
157
|
+
"{job.params['name']}"
|
|
158
|
+
was not found.\n
|
|
159
|
+
Please, select a valid working path.''')
|
|
160
|
+
self.edit_action(job)
|
|
161
|
+
for action in job.sub_actions:
|
|
162
|
+
if 'working_path' in job.params.keys():
|
|
163
|
+
working_path = job.params['working_path']
|
|
164
|
+
if working_path != '' and not os.path.isdir(working_path):
|
|
165
|
+
QMessageBox.warning(self, "Working path not found",
|
|
166
|
+
f'''The working path specified in the project file for the job:
|
|
167
|
+
"{job.params['name']}"
|
|
168
|
+
was not found.\n
|
|
169
|
+
Please, select a valid working path.''')
|
|
170
|
+
self.edit_action(action)
|
|
171
|
+
|
|
172
|
+
def current_file_name(self):
|
|
173
|
+
return os.path.basename(self._current_file) if self._current_file else ''
|
|
174
|
+
|
|
175
|
+
def save_project(self):
|
|
176
|
+
if self._current_file:
|
|
177
|
+
self.do_save(self._current_file)
|
|
178
|
+
else:
|
|
179
|
+
self.save_project_as()
|
|
180
|
+
|
|
181
|
+
def save_project_as(self):
|
|
182
|
+
file_path, _ = QFileDialog.getSaveFileName(self, "Save Project As", "", "Project Files (*.fsp);;All Files (*)")
|
|
183
|
+
if file_path:
|
|
184
|
+
if not file_path.endswith('.fsp'):
|
|
185
|
+
file_path += '.fsp'
|
|
186
|
+
self._current_file_wd = ''
|
|
187
|
+
self.do_save(file_path)
|
|
188
|
+
self._current_file = file_path
|
|
189
|
+
self._modified_project = False
|
|
190
|
+
self.update_title()
|
|
191
|
+
|
|
192
|
+
def do_save(self, file_path):
|
|
193
|
+
try:
|
|
194
|
+
json_obj = jsonpickle.encode({
|
|
195
|
+
'project': self.project.to_dict(),
|
|
196
|
+
'version': 1
|
|
197
|
+
})
|
|
198
|
+
path = f"{self._current_file_wd}/{file_path}" if self._current_file_wd != '' else file_path
|
|
199
|
+
f = open(path, 'w')
|
|
200
|
+
f.write(json_obj)
|
|
201
|
+
self._modified_project = False
|
|
202
|
+
except Exception as e:
|
|
203
|
+
QMessageBox.critical(self, "Error", f"Cannot save file:\n{str(e)}")
|
|
204
|
+
|
|
205
|
+
def _check_unsaved_changes(self) -> bool:
|
|
206
|
+
if self._modified_project:
|
|
207
|
+
reply = QMessageBox.question(
|
|
208
|
+
self, "Unsaved Changes",
|
|
209
|
+
"The project has unsaved changes. Do you want to continue?",
|
|
210
|
+
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
|
|
211
|
+
)
|
|
212
|
+
if reply == QMessageBox.Save:
|
|
213
|
+
self.save_project()
|
|
214
|
+
return True
|
|
215
|
+
elif reply == QMessageBox.Discard:
|
|
216
|
+
return True
|
|
217
|
+
else:
|
|
218
|
+
return False
|
|
219
|
+
else:
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
def on_job_edit(self, item):
|
|
223
|
+
index = self.job_list.row(item)
|
|
224
|
+
if 0 <= index < len(self.project.jobs):
|
|
225
|
+
job = self.project.jobs[index]
|
|
226
|
+
dialog = ActionConfigDialog(job, self)
|
|
227
|
+
if dialog.exec() == QDialog.Accepted:
|
|
228
|
+
current_row = self.job_list.currentRow()
|
|
229
|
+
if current_row >= 0:
|
|
230
|
+
self.job_list.item(current_row).setText(job.params['name'])
|
|
231
|
+
self.refresh_ui()
|
|
232
|
+
|
|
233
|
+
def on_action_edit(self, item):
|
|
234
|
+
job_index = self.job_list.currentRow()
|
|
235
|
+
if 0 <= job_index < len(self.project.jobs):
|
|
236
|
+
job = self.project.jobs[job_index]
|
|
237
|
+
action_index = self.action_list.row(item)
|
|
238
|
+
action_counter = -1
|
|
239
|
+
current_action = None
|
|
240
|
+
is_sub_action = False
|
|
241
|
+
for action in job.sub_actions:
|
|
242
|
+
action_counter += 1
|
|
243
|
+
if action_counter == action_index:
|
|
244
|
+
current_action = action
|
|
245
|
+
break
|
|
246
|
+
if len(action.type_name) > 0:
|
|
247
|
+
for sub_action in action.sub_actions:
|
|
248
|
+
action_counter += 1
|
|
249
|
+
if action_counter == action_index:
|
|
250
|
+
current_action = sub_action
|
|
251
|
+
is_sub_action = True
|
|
252
|
+
break
|
|
253
|
+
if current_action:
|
|
254
|
+
break
|
|
255
|
+
if current_action:
|
|
256
|
+
if not is_sub_action:
|
|
257
|
+
self.set_enabled_sub_actions_gui(current_action.type_name == constants.ACTION_COMBO)
|
|
258
|
+
dialog = ActionConfigDialog(current_action, self)
|
|
259
|
+
if dialog.exec() == QDialog.Accepted:
|
|
260
|
+
self.on_job_selected(job_index)
|
|
261
|
+
self.refresh_ui()
|
|
262
|
+
self.job_list.setCurrentRow(job_index)
|
|
263
|
+
self.action_list.setCurrentRow(action_index)
|
|
264
|
+
|
|
265
|
+
def edit_current_action(self):
|
|
266
|
+
current_action = None
|
|
267
|
+
job_row = self.job_list.currentRow()
|
|
268
|
+
if 0 <= job_row < len(self.project.jobs):
|
|
269
|
+
job = self.project.jobs[job_row]
|
|
270
|
+
if self.job_list.hasFocus():
|
|
271
|
+
current_action = job
|
|
272
|
+
elif self.action_list.hasFocus():
|
|
273
|
+
job_row, action_row, pos = self.get_current_action()
|
|
274
|
+
if pos.actions is not None:
|
|
275
|
+
current_action = pos.action if not pos.is_sub_action else pos.sub_action
|
|
276
|
+
if current_action is not None:
|
|
277
|
+
self.edit_action(current_action)
|
|
278
|
+
|
|
279
|
+
def edit_action(self, action):
|
|
280
|
+
dialog = ActionConfigDialog(action, self)
|
|
281
|
+
if dialog.exec() == QDialog.Accepted:
|
|
282
|
+
self.on_job_selected(self.job_list.currentRow())
|
|
283
|
+
self.mark_as_modified()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from PySide6.QtGui import QColor
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ColorEntry:
|
|
5
|
+
def __init__(self, r, g, b):
|
|
6
|
+
self.r = r
|
|
7
|
+
self.g = g
|
|
8
|
+
self.b = b
|
|
9
|
+
|
|
10
|
+
def tuple(self):
|
|
11
|
+
return self.r, self.g, self.b
|
|
12
|
+
|
|
13
|
+
def hex(self):
|
|
14
|
+
return f"{self.r:02x}{self.g:02x}{self.b:02x}"
|
|
15
|
+
|
|
16
|
+
def q_color(self):
|
|
17
|
+
return QColor(self.r, self.g, self.b)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ColorPalette:
|
|
21
|
+
BLACK = ColorEntry(0, 0, 0)
|
|
22
|
+
WHITE = ColorEntry(255, 255, 255)
|
|
23
|
+
LIGHT_BLUE = ColorEntry(210, 210, 240)
|
|
24
|
+
LIGHT_RED = ColorEntry(240, 210, 210)
|
|
25
|
+
DARK_BLUE = ColorEntry(0, 0, 160)
|
|
26
|
+
DARK_RED = ColorEntry(160, 0, 0)
|
|
27
|
+
MEDIUM_BLUE = ColorEntry(160, 160, 200)
|
|
28
|
+
MEDIUM_GREEN = ColorEntry(160, 200, 160)
|
|
29
|
+
MEDIUM_RED = ColorEntry(200, 160, 160)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
RED_BUTTON_STYLE = f"""
|
|
33
|
+
QPushButton {{
|
|
34
|
+
color: #{ColorPalette.DARK_RED.hex()};
|
|
35
|
+
}}
|
|
36
|
+
QPushButton:disabled {{
|
|
37
|
+
color: #{ColorPalette.MEDIUM_RED.hex()};
|
|
38
|
+
}}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
BLUE_BUTTON_STYLE = f"""
|
|
42
|
+
QPushButton {{
|
|
43
|
+
color: #{ColorPalette.DARK_BLUE.hex()};
|
|
44
|
+
}}
|
|
45
|
+
QPushButton:disabled {{
|
|
46
|
+
color: #{ColorPalette.MEDIUM_BLUE.hex()};
|
|
47
|
+
}}
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
BLUE_COMBO_STYLE = f"""
|
|
51
|
+
QComboBox {{
|
|
52
|
+
color: #{ColorPalette.DARK_BLUE.hex()};
|
|
53
|
+
}}
|
|
54
|
+
QComboBox:disabled {{
|
|
55
|
+
color: #{ColorPalette.MEDIUM_BLUE.hex()};
|
|
56
|
+
}}
|
|
57
|
+
"""
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
from PySide6.QtWidgets import QSizePolicy, QVBoxLayout, QWidget, QLabel, QStackedWidget
|
|
5
|
+
from PySide6.QtPdf import QPdfDocument
|
|
6
|
+
from PySide6.QtPdfWidgets import QPdfView
|
|
7
|
+
from PySide6.QtCore import Qt, QMargins
|
|
8
|
+
from PySide6.QtGui import QPixmap
|
|
9
|
+
from .. config.gui_constants import gui_constants
|
|
10
|
+
from .. core.core_utils import running_under_windows, running_under_macos
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def open_file(file_path):
|
|
14
|
+
try:
|
|
15
|
+
if running_under_macos():
|
|
16
|
+
subprocess.call(('open', file_path))
|
|
17
|
+
elif running_under_windows():
|
|
18
|
+
os.startfile(file_path)
|
|
19
|
+
else:
|
|
20
|
+
subprocess.call(('xdg-open', file_path))
|
|
21
|
+
except Exception:
|
|
22
|
+
webbrowser.open("file://" + file_path)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GuiPdfView(QPdfView):
|
|
26
|
+
def __init__(self, file_path, parent=None):
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
self.file_path = file_path
|
|
29
|
+
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
30
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
31
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
32
|
+
self.setPageSpacing(0)
|
|
33
|
+
self.setDocumentMargins(QMargins(0, 0, 0, 0))
|
|
34
|
+
self.pdf_document = QPdfDocument()
|
|
35
|
+
err = self.pdf_document.load(file_path)
|
|
36
|
+
if err == QPdfDocument.Error.None_:
|
|
37
|
+
self.setDocument(self.pdf_document)
|
|
38
|
+
first_page_size = self.pdf_document.pagePointSize(0)
|
|
39
|
+
zoom_factor = gui_constants.GUI_IMG_WIDTH / first_page_size.width()
|
|
40
|
+
extra_zoom = 0.75 if running_under_windows() else 1.0
|
|
41
|
+
self.setZoomFactor(zoom_factor * extra_zoom)
|
|
42
|
+
self.setFixedSize(gui_constants.GUI_IMG_WIDTH,
|
|
43
|
+
int(first_page_size.height() * zoom_factor))
|
|
44
|
+
else:
|
|
45
|
+
raise RuntimeError(f"Can't load file: {file_path}. Error code: {err}.")
|
|
46
|
+
self.setStyleSheet('''
|
|
47
|
+
QWidget {
|
|
48
|
+
border: 2px solid #0000a0;
|
|
49
|
+
}
|
|
50
|
+
QWidget:hover {
|
|
51
|
+
border: 2px solid #a0a0ff;
|
|
52
|
+
}
|
|
53
|
+
''')
|
|
54
|
+
|
|
55
|
+
def sizeHint(self):
|
|
56
|
+
return self.size()
|
|
57
|
+
|
|
58
|
+
def mouseReleaseEvent(self, event):
|
|
59
|
+
if event.button() == Qt.LeftButton:
|
|
60
|
+
open_file(self.file_path)
|
|
61
|
+
super().mouseReleaseEvent(event)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GuiImageView(QWidget):
|
|
65
|
+
def __init__(self, file_path, parent=None):
|
|
66
|
+
super().__init__(parent)
|
|
67
|
+
self.file_path = file_path
|
|
68
|
+
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
69
|
+
self.layout = QVBoxLayout()
|
|
70
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
71
|
+
self.layout.setSpacing(0)
|
|
72
|
+
self.image_label = QLabel()
|
|
73
|
+
self.image_label.setAlignment(Qt.AlignCenter)
|
|
74
|
+
self.layout.addWidget(self.image_label)
|
|
75
|
+
self.setLayout(self.layout)
|
|
76
|
+
pixmap = QPixmap(file_path)
|
|
77
|
+
if pixmap:
|
|
78
|
+
scaled_pixmap = pixmap.scaledToWidth(gui_constants.GUI_IMG_WIDTH, Qt.SmoothTransformation)
|
|
79
|
+
self.image_label.setPixmap(scaled_pixmap)
|
|
80
|
+
else:
|
|
81
|
+
raise RuntimeError(f"Can't load file: {file_path}.")
|
|
82
|
+
self.setStyleSheet('''
|
|
83
|
+
QWidget {
|
|
84
|
+
border: 2px solid #0000a0;
|
|
85
|
+
}
|
|
86
|
+
QWidget:hover {
|
|
87
|
+
border: 2px solid #a0a0ff;
|
|
88
|
+
}
|
|
89
|
+
''')
|
|
90
|
+
|
|
91
|
+
def sizeHint(self):
|
|
92
|
+
return self.size()
|
|
93
|
+
|
|
94
|
+
def mouseReleaseEvent(self, event):
|
|
95
|
+
if event.button() == Qt.LeftButton:
|
|
96
|
+
open_file(self.file_path)
|
|
97
|
+
super().mouseReleaseEvent(event)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class GuiOpenApp(QWidget):
|
|
101
|
+
def __init__(self, app, file_path, parent=None):
|
|
102
|
+
super().__init__(parent)
|
|
103
|
+
self.file_path = file_path
|
|
104
|
+
self.app = app
|
|
105
|
+
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
106
|
+
self.layout = QVBoxLayout()
|
|
107
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
108
|
+
self.layout.setSpacing(0)
|
|
109
|
+
self.image_label = QLabel()
|
|
110
|
+
self.image_label.setAlignment(Qt.AlignCenter)
|
|
111
|
+
self.layout.addWidget(self.image_label)
|
|
112
|
+
self.setLayout(self.layout)
|
|
113
|
+
pixmap = QPixmap(file_path)
|
|
114
|
+
if pixmap:
|
|
115
|
+
scaled_pixmap = pixmap.scaledToWidth(gui_constants.GUI_IMG_WIDTH, Qt.SmoothTransformation)
|
|
116
|
+
self.image_label.setPixmap(scaled_pixmap)
|
|
117
|
+
else:
|
|
118
|
+
raise RuntimeError(f"Can't load file: {file_path}.")
|
|
119
|
+
self.setStyleSheet('''
|
|
120
|
+
QWidget {
|
|
121
|
+
border: 2px solid #a00000;
|
|
122
|
+
}
|
|
123
|
+
QWidget:hover {
|
|
124
|
+
border: 2px solid #ffa0a0;
|
|
125
|
+
}
|
|
126
|
+
''')
|
|
127
|
+
|
|
128
|
+
def sizeHint(self):
|
|
129
|
+
return self.size()
|
|
130
|
+
|
|
131
|
+
def mouseReleaseEvent(self, event):
|
|
132
|
+
if event.button() == Qt.LeftButton:
|
|
133
|
+
if self.app != 'internal_retouch_app':
|
|
134
|
+
try:
|
|
135
|
+
os.system(f"{self.app} -f {self.file_path} &")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise RuntimeError(f"Can't open file {self.file_path} with app: {self.app}.\n{str(e)}")
|
|
138
|
+
else:
|
|
139
|
+
app = None
|
|
140
|
+
stacked_widget = self.parent().window().findChild(QStackedWidget)
|
|
141
|
+
if stacked_widget:
|
|
142
|
+
for child in stacked_widget.children():
|
|
143
|
+
if child.metaObject().className() == 'MainWindow':
|
|
144
|
+
app = child
|
|
145
|
+
break
|
|
146
|
+
else:
|
|
147
|
+
raise RuntimeError("MainWindow object not found")
|
|
148
|
+
if app:
|
|
149
|
+
app.retouch_callback(self.file_path)
|
|
150
|
+
else:
|
|
151
|
+
raise RuntimeError("MainWindow object not found")
|
|
152
|
+
super().mouseReleaseEvent(event)
|