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.

Files changed (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,213 @@
1
+ import logging
2
+ from PySide6.QtWidgets import QWidget, QTextEdit, QMessageBox, QStatusBar
3
+ from PySide6.QtGui import QTextCursor, QTextOption, QFont
4
+ from PySide6.QtCore import QThread, QObject, Signal, Slot, Qt
5
+ from .. config.constants import constants
6
+
7
+
8
+ class SimpleHtmlFormatter(logging.Formatter):
9
+ COLOR_MAP = {
10
+ 'DEBUG': '#5c85d6', # light blue
11
+ 'INFO': '#50c878', # green
12
+ 'WARNING': '#ffcc00', # yellow
13
+ 'ERROR': '#ff3333', # red
14
+ 'CRITICAL': '#cc0066' # dark red
15
+ }
16
+ FF = '80'
17
+ OO = '00'
18
+ MM = '40'
19
+ ANSI_COLORS = {
20
+ # Reset
21
+ '\x1b[0m': '</span>',
22
+ '\x1b[m': '</span>',
23
+ # Colori base (30-37)
24
+ '\x1b[30m': f'<span style="color:#{OO}{OO}{OO}">', # black
25
+ '\x1b[31m': f'<span style="color:#{FF}{OO}{OO}">', # red
26
+ '\x1b[32m': f'<span style="color:#{OO}{FF}{OO}">', # green
27
+ '\x1b[33m': f'<span style="color:#{FF}{FF}{OO}">', # yellow
28
+ '\x1b[34m': f'<span style="color:#{OO}{OO}{FF}">', # blue
29
+ '\x1b[35m': f'<span style="color:#{FF}{OO}{FF}">', # magenta
30
+ '\x1b[36m': f'<span style="color:#{OO}{FF}{FF}">', # cyan
31
+ '\x1b[37m': f'<span style="color:#{FF}{FF}{FF}">', # white
32
+ # Brilliant colors (90-97)
33
+ '\x1b[90m': f'<span style="color:#{MM}{MM}{MM}">',
34
+ '\x1b[91m': f'<span style="color:#{FF}{MM}{MM}">',
35
+ '\x1b[92m': f'<span style="color:#{MM}{FF}{MM}">',
36
+ '\x1b[93m': f'<span style="color:#{FF}{FF}{MM}">',
37
+ '\x1b[94m': f'<span style="color:#{MM}{MM}{FF}">',
38
+ '\x1b[95m': f'<span style="color:#{FF}{MM}{FF}">',
39
+ '\x1b[96m': f'<span style="color:#{MM}{FF}{FF}">',
40
+ '\x1b[97m': f'<span style="color:#{FF}{FF}{FF}">',
41
+ # Background (40-47)
42
+ '\x1b[40m': f'<span style="background-color:#{OO}{OO}{OO}">',
43
+ '\x1b[41m': f'<span style="background-color:#{FF}{OO}{OO}">',
44
+ '\x1b[42m': f'<span style="background-color:#{OO}{FF}{OO}">',
45
+ '\x1b[43m': f'<span style="background-color:#{FF}{FF}{OO}">',
46
+ '\x1b[44m': f'<span style="background-color:#{OO}{OO}{FF}">',
47
+ '\x1b[45m': f'<span style="background-color:#{FF}{OO}{FF}">',
48
+ '\x1b[46m': f'<span style="background-color:#{OO}{FF}{FF}">',
49
+ '\x1b[47m': f'<span style="background-color:#{FF}{FF}{FF}">',
50
+ # Styles
51
+ '\x1b[1m': '<span style="font-weight:bold">', # bold
52
+ '\x1b[3m': '<span style="font-style:italic">', # italis
53
+ '\x1b[4m': '<span style="text-decoration:underline">', # underline
54
+ }
55
+
56
+ def __init__(self, fmt=None, datefmt=None, style='%'):
57
+ super().__init__()
58
+ self._fmt = fmt or "[%(levelname).3s] %(message)s"
59
+ self.datefmt = datefmt or "%H:%M:%S"
60
+
61
+ def format(self, record):
62
+ levelname = record.levelname
63
+ message = super().format(record)
64
+ message = message.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
65
+ for ansi_code, html_tag in self.ANSI_COLORS.items():
66
+ message = message.replace(ansi_code, html_tag)
67
+ message = constants.ANSI_ESCAPE.sub('', message).replace("\r", "").rstrip()
68
+ color = self.COLOR_MAP.get(levelname, '#000000')
69
+ return f'''
70
+ <div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
71
+ <span style="color: {color}; font-weight: bold;">[{levelname[:3]}] </span>
72
+ <span>{message}</span>
73
+ </div>
74
+ '''
75
+
76
+
77
+ class SimpleHtmlHandler(QObject, logging.Handler):
78
+ log_signal = Signal(str)
79
+ html_signal = Signal(str)
80
+
81
+ def __init__(self):
82
+ QObject.__init__(self)
83
+ logging.Handler.__init__(self)
84
+ self.setFormatter(SimpleHtmlFormatter())
85
+
86
+ def emit(self, record):
87
+ try:
88
+ msg = self.format(record)
89
+ self.html_signal.emit(msg)
90
+ except Exception as e:
91
+ logging.error(f"Logging error: {e}")
92
+
93
+
94
+ class GuiLogger(QWidget):
95
+ __id_counter = 0
96
+
97
+ def __init__(self, parent=None):
98
+ super().__init__(parent)
99
+ self.id = self.__class__.__id_counter
100
+ self.__class__.__id_counter += 1
101
+
102
+ def id_str(self):
103
+ return f"{self.__class__.__name__}_{self.id}"
104
+
105
+ @Slot(str, str)
106
+ def handle_log_message(self, level, message):
107
+ logger = logging.getLogger(self.id_str())
108
+ log_func = {
109
+ "INFO": logger.info,
110
+ "WARNING": logger.warning,
111
+ "DEBUG": logger.debug,
112
+ "ERROR": logger.error,
113
+ "CRITICAL": logger.critical,
114
+ }.get(level, logger.info)
115
+ log_func(message)
116
+
117
+
118
+ class QTextEditLogger(GuiLogger):
119
+ def __init__(self, parent=None):
120
+ super().__init__(parent)
121
+ text_edit = QTextEdit(self)
122
+ text_edit.setWordWrapMode(QTextOption.WrapMode.WordWrap)
123
+ text_edit.setAcceptRichText(True)
124
+ text_edit.setReadOnly(True)
125
+ font = QFont(constants.LOG_FONTS, 12)
126
+ text_edit.setFont(font)
127
+ self.text_edit = text_edit
128
+ self.status_bar = QStatusBar()
129
+
130
+ @Slot(str)
131
+ def handle_html_message(self, html):
132
+ self.append_html(html)
133
+
134
+ @Slot(str)
135
+ def append_html(self, html):
136
+ self.text_edit.append(html)
137
+ cursor = self.text_edit.textCursor()
138
+ cursor.movePosition(QTextCursor.End)
139
+ self.text_edit.setTextCursor(cursor)
140
+ self.text_edit.ensureCursorVisible()
141
+
142
+ @Slot(str, int, str, int)
143
+ def handle_status_message(self, message, status, error_message, timeout):
144
+ if status == constants.RUN_FAILED:
145
+ QMessageBox.critical(self, "Error", f"Job failed.\n{error_message}")
146
+ elif status == constants.RUN_STOPPED:
147
+ QMessageBox.warning(self, "Warning", "Run stopped.")
148
+ self.status_bar.showMessage(message, timeout)
149
+
150
+ @Slot(str)
151
+ def handle_exception(self, message):
152
+ QMessageBox.warning(None, "Error", message)
153
+
154
+
155
+ class LogWorker(QThread):
156
+ log_signal = Signal(str, str)
157
+ html_signal = Signal(str)
158
+ end_signal = Signal(int, str, str)
159
+ status_signal = Signal(str, int, str, int)
160
+ exception_signal = Signal(str)
161
+
162
+ def run(self):
163
+ pass
164
+
165
+
166
+ class LogManager:
167
+ def __init__(self):
168
+ self.gui_loggers = {}
169
+ self.last_gui_logger = None
170
+ self.handler = None
171
+ self.log_worker = None
172
+ self.id = -1
173
+
174
+ def last_id(self):
175
+ return self.last_gui_logger.id if self.last_gui_logger else -1
176
+
177
+ def last_id_str(self):
178
+ return self.last_gui_logger.id_str() if self.last_gui_logger else ""
179
+
180
+ def add_gui_logger(self, gui_logger: GuiLogger):
181
+ self.gui_loggers[gui_logger.id] = gui_logger
182
+ self.last_gui_logger = gui_logger
183
+
184
+ def start_thread(self, worker: LogWorker):
185
+ if len(self.gui_loggers) == 0:
186
+ raise RuntimeError("No text edit widgets registered")
187
+ self.before_thread_begins()
188
+ self.id = self.last_id()
189
+ logger = logging.getLogger(self.last_id_str())
190
+ logger.setLevel(logging.DEBUG)
191
+ gui_logger = self.gui_loggers[self.id]
192
+ self.handler = SimpleHtmlHandler()
193
+ self.handler.setLevel(logging.DEBUG)
194
+ logger.addHandler(self.handler)
195
+ self.handler.log_signal.connect(gui_logger.append_html, Qt.QueuedConnection)
196
+ self.handler.html_signal.connect(gui_logger.handle_html_message, Qt.QueuedConnection)
197
+ self.log_worker = worker
198
+ self.log_worker.log_signal.connect(gui_logger.handle_log_message, Qt.QueuedConnection)
199
+ self.log_worker.html_signal.connect(gui_logger.handle_html_message, Qt.QueuedConnection)
200
+ self.log_worker.status_signal.connect(gui_logger.handle_status_message, Qt.QueuedConnection)
201
+ self.log_worker.exception_signal.connect(gui_logger.handle_exception, Qt.QueuedConnection)
202
+ self.log_worker.end_signal.connect(self.handle_end_message, Qt.QueuedConnection)
203
+ self.log_worker.start()
204
+
205
+ def before_thread_begins(self):
206
+ pass
207
+
208
+ def do_handle_end_message(self, status, id_str, message):
209
+ pass
210
+
211
+ @Slot(int, str, str)
212
+ def handle_end_message(self, status, id_str, message):
213
+ self.do_handle_end_message(status, id_str, message)
@@ -0,0 +1,393 @@
1
+ import time
2
+ from PySide6.QtWidgets import (QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QProgressBar,
3
+ QMessageBox, QScrollArea, QSizePolicy, QFrame, QLabel, QComboBox)
4
+ from PySide6.QtGui import QColor
5
+ from PySide6.QtCore import Qt, QTimer
6
+ from PySide6.QtCore import Signal, Slot
7
+ from .. config.constants import constants
8
+ from .. config.gui_constants import gui_constants
9
+ from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
10
+ from .gui_logging import LogWorker, QTextEditLogger
11
+ from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
12
+ from .colors import ColorPalette
13
+
14
+
15
+ class ColorButton(QPushButton):
16
+ def __init__(self, text, enabled, parent=None):
17
+ super().__init__(text.replace(gui_constants.DISABLED_TAG, ''), parent)
18
+ self.setMinimumHeight(1)
19
+ self.setMaximumHeight(70)
20
+ color = ColorPalette.LIGHT_BLUE if enabled else ColorPalette.LIGHT_RED
21
+ self.set_color(*color.tuple())
22
+
23
+ def set_color(self, r, g, b):
24
+ self.color = QColor(r, g, b)
25
+ self.setStyleSheet(f"""
26
+ QPushButton {{
27
+ background-color: {self.color.name()};
28
+ color: #{ColorPalette.DARK_BLUE.hex()};
29
+ font-weight: bold;
30
+ border: none;
31
+ min-height: 1px;
32
+ padding: 4px;
33
+ margin: 0px;
34
+ }}
35
+ """)
36
+
37
+
38
+ action_running_color = ColorPalette.MEDIUM_BLUE
39
+ action_done_color = ColorPalette.MEDIUM_GREEN
40
+
41
+
42
+ class TimerProgressBar(QProgressBar):
43
+ light_background_color = ColorPalette.LIGHT_BLUE
44
+ border_color = ColorPalette.DARK_BLUE
45
+ text_color = ColorPalette.DARK_BLUE
46
+
47
+ def __init__(self):
48
+ super().__init__()
49
+ super().setRange(0, 10)
50
+ super().setValue(0)
51
+ self.set_running_style()
52
+ self._start_time = -1
53
+
54
+ def set_style(self, bar_color=None):
55
+ if bar_color is None:
56
+ bar_color = ColorPalette.MEDIUM_BLUE
57
+ self.setStyleSheet(f"""
58
+ QProgressBar {{
59
+ border: 2px solid #{self.border_color.hex()};
60
+ border-radius: 8px;
61
+ text-align: center;
62
+ font-weight: bold;
63
+ font-size: 12px;
64
+ background-color: #{self.light_background_color.hex()};
65
+ color: #{self.text_color.hex()};
66
+ min-height: 1px;
67
+ }}
68
+ QProgressBar::chunk {{
69
+ border-radius: 6px;
70
+ background-color: #{bar_color.hex()};
71
+ }}
72
+ """)
73
+
74
+ def time_str(self, secs):
75
+ ss = int(secs)
76
+ h = ss // 3600
77
+ m = (ss % 3600) // 60
78
+ s = (ss % 3600) % 60
79
+ x = secs - ss
80
+ t_str = "{:02d}".format(s) + "{:.1f}s".format(x).lstrip('0')
81
+ if m > 0:
82
+ t_str = "{:02d}:{}".format(m, t_str)
83
+ if h > 0:
84
+ t_str = "{:02d}:{}".format(h, t_str)
85
+ if m > 0 or h > 0:
86
+ t_str = t_str.lstrip('0')
87
+ elif 0 < s < 10:
88
+ t_str = t_str.lstrip('0')
89
+ elif s == 0:
90
+ t_str = t_str[1:]
91
+ return t_str
92
+
93
+ def check_time(self, val):
94
+ if self._start_time < 0:
95
+ raise RuntimeError("TimeProgressbar: start and must be called before setValue and stop")
96
+ self._current_time = time.time()
97
+ elapsed_time = self._current_time - self._start_time
98
+ elapsed_str = self.time_str(elapsed_time)
99
+ fmt = f"Progress: %p% - %v of %m - elapsed: {elapsed_str}"
100
+ if 0 < val < self.maximum():
101
+ time_per_iter = elapsed_time / val
102
+ estimated_time = time_per_iter * self.maximum()
103
+ remaining_time = max(0, estimated_time - elapsed_time)
104
+ remaining_str = self.time_str(remaining_time)
105
+ fmt += f", {remaining_str} remaining"
106
+ self.setFormat(fmt)
107
+
108
+ def start(self, steps):
109
+ super().setMaximum(steps)
110
+ self._start_time = time.time()
111
+ self.setValue(0)
112
+
113
+ def stop(self):
114
+ self.check_time(self.maximum())
115
+ self.setValue(self.maximum())
116
+
117
+ def setValue(self, val):
118
+ self.check_time(val)
119
+ super().setValue(val)
120
+
121
+ def set_running_style(self):
122
+ self.set_style(action_running_color)
123
+
124
+ def set_done_style(self):
125
+ self.set_style(action_done_color)
126
+
127
+
128
+ class RunWindow(QTextEditLogger):
129
+ def __init__(self, labels, stop_worker_callback, close_window_callback, retouch_paths, parent):
130
+ QTextEditLogger.__init__(self, parent)
131
+ self.retouch_paths = retouch_paths
132
+ self.stop_worker_callback = stop_worker_callback
133
+ self.close_window_callback = close_window_callback
134
+ self.row_widget_id = 0
135
+ layout = QVBoxLayout()
136
+ self.color_widgets = []
137
+ self.image_views = []
138
+ if len(labels) > 0:
139
+ for label_row in labels:
140
+ self.color_widgets.append([])
141
+ row = QWidget(self)
142
+ h_layout = QHBoxLayout(row)
143
+ h_layout.setContentsMargins(0, 0, 0, 0)
144
+ h_layout.setSpacing(2)
145
+ for label, enabled in label_row:
146
+ widget = ColorButton(label, enabled)
147
+ h_layout.addWidget(widget, stretch=1)
148
+ self.color_widgets[-1].append(widget)
149
+ layout.addWidget(row)
150
+ self.progress_bar = TimerProgressBar()
151
+ layout.addWidget(self.progress_bar)
152
+ output_layout = QHBoxLayout()
153
+ left_layout, right_layout = QVBoxLayout(), QVBoxLayout()
154
+ output_layout.addLayout(left_layout, stretch=1)
155
+ output_layout.addLayout(right_layout, stretch=0)
156
+ left_layout.addWidget(self.text_edit)
157
+ self.right_area = QScrollArea()
158
+ self.right_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
159
+ self.right_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
160
+ self.right_area.setWidgetResizable(True)
161
+ self.right_area.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
162
+ self.right_area.setContentsMargins(0, 0, 0, 0)
163
+ self.right_area.setFrameShape(QFrame.NoFrame)
164
+ self.right_area.setViewportMargins(0, 0, 0, 0)
165
+ self.right_area.viewport().setStyleSheet("background: transparent; border: 0px;")
166
+ self.image_area_widget = QWidget()
167
+ self.image_area_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
168
+ self.image_area_widget.setContentsMargins(0, 0, 0, 0)
169
+ self.right_area.setWidget(self.image_area_widget)
170
+ self.image_layout = QVBoxLayout()
171
+ self.image_layout.setSpacing(5)
172
+ self.image_layout.setContentsMargins(0, 0, 0, 0)
173
+ self.image_layout.setAlignment(Qt.AlignTop)
174
+ self.image_area_widget.setLayout(self.image_layout)
175
+ right_layout.addWidget(self.right_area)
176
+ right_layout.setContentsMargins(0, 0, 0, 0)
177
+ self.right_area.setMinimumWidth(0)
178
+ self.right_area.setMaximumWidth(0)
179
+ self.image_area_widget.setFixedWidth(0)
180
+ layout.addLayout(output_layout)
181
+
182
+ n_paths = len(self.retouch_paths) if self.retouch_paths else 0
183
+ if n_paths == 1:
184
+ self.retouch_widget = QPushButton(f"Retouch {self.retouch_paths[0][0]}")
185
+ self.retouch_widget.setStyleSheet(BLUE_BUTTON_STYLE)
186
+ self.retouch_widget.setEnabled(False)
187
+ self.retouch_widget.clicked.connect(lambda: self.retouch(self.retouch_paths[0]))
188
+ self.status_bar.addPermanentWidget(self.retouch_widget)
189
+ elif n_paths > 1:
190
+ options = ["Retouch:"] + [f"{path[0]}" for path in self.retouch_paths]
191
+ self.retouch_widget = QComboBox()
192
+ self.retouch_widget.setStyleSheet(BLUE_COMBO_STYLE)
193
+ self.retouch_widget.addItems(options)
194
+ self.retouch_widget.setEnabled(False)
195
+ self.retouch_widget.currentIndexChanged.connect(lambda: self.retouch(self.retouch_paths[self.retouch_widget.currentIndex() - 1]))
196
+ self.status_bar.addPermanentWidget(self.retouch_widget)
197
+
198
+ self.stop_button = QPushButton("Stop")
199
+ self.stop_button.setStyleSheet(RED_BUTTON_STYLE)
200
+ self.stop_button.clicked.connect(self.stop_worker)
201
+ self.status_bar.addPermanentWidget(self.stop_button)
202
+
203
+ self.close_button = QPushButton("Close")
204
+ self.close_button.setEnabled(False)
205
+ self.close_button.setStyleSheet(RED_BUTTON_STYLE)
206
+ self.close_button.clicked.connect(self.close_window)
207
+ self.status_bar.addPermanentWidget(self.close_button)
208
+
209
+ layout.addWidget(self.status_bar)
210
+ self.setLayout(layout)
211
+
212
+ def stop_worker(self):
213
+ self.stop_worker_callback(self.id_str())
214
+
215
+ def retouch(self, path):
216
+
217
+ def find_parent(widget, class_name):
218
+ current = widget
219
+ while current is not None:
220
+ if current.__class__.__name__ == class_name:
221
+ return current
222
+ current = current.parent()
223
+ return None
224
+ parent = find_parent(self, "MainWindow")
225
+ if parent:
226
+ parent.retouch_callback(path[1])
227
+ else:
228
+ raise RuntimeError("Can't find MainWindow parent.")
229
+
230
+ def close_window(self):
231
+ confirm = QMessageBox()
232
+ confirm.setIcon(QMessageBox.Question)
233
+ confirm.setWindowTitle('Close Tab')
234
+ confirm.setInformativeText("Really close tab?")
235
+ confirm.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
236
+ confirm.setDefaultButton(QMessageBox.Cancel)
237
+ if confirm.exec() == QMessageBox.Ok:
238
+ self.close_window_callback(self.id_str())
239
+
240
+ @Slot(int, str)
241
+ def handle_before_action(self, id, name):
242
+ if 0 <= id < len(self.color_widgets[self.row_widget_id]):
243
+ self.color_widgets[self.row_widget_id][id].set_color(*action_running_color.tuple())
244
+ self.progress_bar.start(1)
245
+ if id == -1:
246
+ self.progress_bar.set_running_style()
247
+
248
+ @Slot(int, str)
249
+ def handle_after_action(self, id, name):
250
+ if 0 <= id < len(self.color_widgets[self.row_widget_id]):
251
+ self.color_widgets[self.row_widget_id][id].set_color(*action_done_color.tuple())
252
+ self.progress_bar.stop()
253
+ if id == -1:
254
+ self.row_widget_id += 1
255
+ self.progress_bar.set_done_style()
256
+
257
+ @Slot(int, str, str)
258
+ def handle_step_counts(self, id, name, steps):
259
+ self.progress_bar.start(steps)
260
+
261
+ @Slot(int, str)
262
+ def handle_begin_steps(self, id, name):
263
+ self.progress_bar.start(1)
264
+
265
+ @Slot(int, str)
266
+ def handle_end_steps(self, id, name):
267
+ self.progress_bar.stop()
268
+
269
+ @Slot(int, str, str)
270
+ def handle_after_step(self, id, name, step):
271
+ self.progress_bar.setValue(step)
272
+
273
+ @Slot(int, str, str)
274
+ def handle_save_plot(self, id, name, path):
275
+ label = QLabel(name, self)
276
+ label.setStyleSheet("QLabel {margin-top: 5px; font-weight: bold;}")
277
+ self.image_layout.addWidget(label)
278
+ ext = path.split('.')[-1].lower()
279
+ if ext == 'pdf':
280
+ image_view = GuiPdfView(path, self)
281
+ elif ext in ['jpg', 'jpeg', 'tif', 'tiff', 'png']:
282
+ image_view = GuiImageView(path, self)
283
+ else:
284
+ raise RuntimeError("Can't visualize file type {ext}.")
285
+ self.image_views.append(image_view)
286
+ self.image_layout.addWidget(image_view)
287
+ max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
288
+ needed_width = max_width + 20
289
+ self.right_area.setFixedWidth(needed_width)
290
+ self.image_area_widget.setFixedWidth(needed_width)
291
+ self.right_area.updateGeometry()
292
+ self.image_area_widget.updateGeometry()
293
+ QTimer.singleShot(0, lambda: self.right_area.verticalScrollBar().setValue(self.right_area.verticalScrollBar().maximum()))
294
+
295
+ @Slot(int, str, str, str)
296
+ def handle_open_app(self, id, name, app, path):
297
+ label = QLabel(name, self)
298
+ label.setStyleSheet("QLabel {margin-top: 5px; font-weight: bold;}")
299
+ self.image_layout.addWidget(label)
300
+ image_view = GuiOpenApp(app, path, self)
301
+ self.image_views.append(image_view)
302
+ self.image_layout.addWidget(image_view)
303
+ max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
304
+ needed_width = max_width + 15
305
+ self.right_area.setFixedWidth(needed_width)
306
+ self.image_area_widget.setFixedWidth(needed_width)
307
+ self.right_area.updateGeometry()
308
+ self.image_area_widget.updateGeometry()
309
+ QTimer.singleShot(0, lambda: self.right_area.verticalScrollBar().setValue(self.right_area.verticalScrollBar().maximum()))
310
+
311
+
312
+ class RunWorker(LogWorker):
313
+ before_action_signal = Signal(int, str)
314
+ after_action_signal = Signal(int, str)
315
+ step_counts_signal = Signal(int, str, int)
316
+ begin_steps_signal = Signal(int, str)
317
+ end_steps_signal = Signal(int, str)
318
+ after_step_signal = Signal(int, str, int)
319
+ save_plot_signal = Signal(int, str, str)
320
+ open_app_signal = Signal(int, str, str, str)
321
+
322
+ def __init__(self, id_str):
323
+ LogWorker.__init__(self)
324
+ self.id_str = id_str
325
+ self.status = constants.STATUS_RUNNING
326
+ self.callbacks = {
327
+ 'before_action': self.before_action,
328
+ 'after_action': self.after_action,
329
+ 'step_counts': self.step_counts,
330
+ 'begin_steps': self.begin_steps,
331
+ 'end_steps': self.end_steps,
332
+ 'after_step': self.after_step,
333
+ 'save_plot': self.save_plot,
334
+ 'check_running': self.check_running,
335
+ 'open_app': self.open_app
336
+ }
337
+ self.tag = ""
338
+
339
+ def before_action(self, id, name):
340
+ self.before_action_signal.emit(id, name)
341
+
342
+ def after_action(self, id, name):
343
+ self.after_action_signal.emit(id, name)
344
+
345
+ def step_counts(self, id, name, steps):
346
+ self.step_counts_signal.emit(id, name, steps)
347
+
348
+ def begin_steps(self, id, name):
349
+ self.begin_steps_signal.emit(id, name)
350
+
351
+ def end_steps(self, id, name):
352
+ self.end_steps_signal.emit(id, name)
353
+
354
+ def after_step(self, id, name, step):
355
+ self.after_step_signal.emit(id, name, step)
356
+
357
+ def save_plot(self, id, name, path):
358
+ self.save_plot_signal.emit(id, name, path)
359
+
360
+ def open_app(self, id, name, app, path):
361
+ self.open_app_signal.emit(id, name, app, path)
362
+
363
+ def check_running(self, id, name):
364
+ return self.status == constants.STATUS_RUNNING
365
+
366
+ def run(self):
367
+ self.status_signal.emit(f"{self.tag} running...", constants.RUN_ONGOING, "", 0)
368
+ self.html_signal.emit(f'''
369
+ <div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
370
+ <span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weigt: bold;">{self.tag} begins</span>
371
+ </div>
372
+ ''')
373
+ status, error_message = self.do_run()
374
+ if status == constants.RUN_FAILED:
375
+ message = f"{self.tag} failed"
376
+ color = "#" + ColorPalette.DARK_RED.hex()
377
+ elif status == constants.RUN_COMPLETED:
378
+ message = f"{self.tag} ended successfully"
379
+ color = "#" + ColorPalette.DARK_BLUE.hex()
380
+ elif status == constants.RUN_STOPPED:
381
+ message = f"{self.tag} stopped"
382
+ color = "#" + ColorPalette.DARK_RED.hex()
383
+ self.html_signal.emit(f'''
384
+ <div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
385
+ <span style="color: {color}; font-style: italic; font-weight: bold;">{message}</span>
386
+ </div>
387
+ ''')
388
+ self.end_signal.emit(status, self.id_str, message)
389
+ self.status_signal.emit(message, status, error_message, 0)
390
+
391
+ def stop(self):
392
+ self.status = constants.STATUS_STOPPED
393
+ self.wait()