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,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('&', '&').replace('<', '<').replace('>', '>')
|
|
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()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|