shinestacker 1.5.4__py3-none-any.whl → 1.6.1__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/multilayer.py +1 -1
- shinestacker/algorithms/stack.py +17 -9
- shinestacker/app/args_parser_opts.py +4 -0
- shinestacker/app/gui_utils.py +10 -2
- shinestacker/app/main.py +8 -3
- shinestacker/app/project.py +7 -3
- shinestacker/app/retouch.py +8 -1
- shinestacker/app/settings_dialog.py +171 -0
- shinestacker/config/app_config.py +30 -0
- shinestacker/config/constants.py +3 -0
- shinestacker/config/gui_constants.py +4 -2
- shinestacker/config/settings.py +110 -0
- shinestacker/core/core_utils.py +3 -12
- shinestacker/core/logging.py +3 -2
- shinestacker/gui/action_config.py +6 -5
- shinestacker/gui/action_config_dialog.py +17 -74
- shinestacker/gui/config_dialog.py +78 -0
- shinestacker/gui/main_window.py +6 -6
- shinestacker/gui/menu_manager.py +2 -0
- shinestacker/gui/new_project.py +2 -1
- shinestacker/gui/project_controller.py +8 -6
- shinestacker/gui/project_model.py +16 -1
- shinestacker/gui/recent_file_manager.py +3 -21
- shinestacker/retouch/base_filter.py +1 -1
- shinestacker/retouch/display_manager.py +48 -7
- shinestacker/retouch/image_editor_ui.py +27 -36
- shinestacker/retouch/image_view_status.py +4 -1
- shinestacker/retouch/image_viewer.py +17 -9
- shinestacker/retouch/io_gui_handler.py +96 -44
- shinestacker/retouch/io_threads.py +78 -0
- shinestacker/retouch/layer_collection.py +12 -0
- shinestacker/retouch/overlaid_view.py +13 -5
- shinestacker/retouch/paint_area_manager.py +30 -0
- shinestacker/retouch/sidebyside_view.py +32 -16
- shinestacker/retouch/transformation_manager.py +1 -3
- shinestacker/retouch/undo_manager.py +15 -13
- shinestacker/retouch/view_strategy.py +79 -26
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/METADATA +1 -1
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/RECORD +44 -39
- shinestacker/retouch/io_manager.py +0 -69
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.
|
|
1
|
+
__version__ = '1.6.1'
|
|
@@ -174,7 +174,7 @@ class MultiLayer(TaskBase, ImageSequenceManager):
|
|
|
174
174
|
if self.exif_path == '':
|
|
175
175
|
self.exif_path = job.action_path(0)
|
|
176
176
|
if self.exif_path != '':
|
|
177
|
-
self.exif_path = self.working_path
|
|
177
|
+
self.exif_path = os.path.join(self.working_path, self.exif_path)
|
|
178
178
|
|
|
179
179
|
def run_core(self):
|
|
180
180
|
if isinstance(self.input_full_path(), str):
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, R0913, R0917
|
|
2
2
|
import os
|
|
3
|
-
import
|
|
3
|
+
import traceback
|
|
4
4
|
from .. config.constants import constants
|
|
5
5
|
from .. core.framework import TaskBase
|
|
6
6
|
from .. core.colors import color_str
|
|
@@ -34,13 +34,19 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
|
34
34
|
self.sub_message_r(': denoise image')
|
|
35
35
|
stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
|
|
36
36
|
write_img(out_filename, stacked_img)
|
|
37
|
-
if self.exif_path != ''
|
|
38
|
-
self.sub_message_r(': copy exif data')
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
if self.exif_path != '':
|
|
38
|
+
self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
|
|
39
|
+
if not os.path.exists(self.exif_path):
|
|
40
|
+
raise RuntimeError(f"Path {self.exif_path} does not exist.")
|
|
41
|
+
try:
|
|
42
|
+
_dirpath, _, fnames = next(os.walk(self.exif_path))
|
|
43
|
+
fnames = [name for name in fnames if extension_tif_jpg(name)]
|
|
44
|
+
exif_filename = os.path.join(self.exif_path, fnames[0])
|
|
45
|
+
copy_exif_from_file_to_file(exif_filename, out_filename)
|
|
46
|
+
self.sub_message_r(' ' * 60)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
traceback.print_tb(e.__traceback__)
|
|
49
|
+
raise RuntimeError("Can't copy EXIF data") from e
|
|
44
50
|
if self.plot_stack:
|
|
45
51
|
idx_str = f"{self.frame_count + 1:04d}" if self.frame_count >= 0 else ''
|
|
46
52
|
name = f"{self.name}: {self.stack_algo.name()}"
|
|
@@ -51,11 +57,13 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
|
51
57
|
self.frame_count += 1
|
|
52
58
|
|
|
53
59
|
def init(self, job, working_path=''):
|
|
60
|
+
if working_path == '':
|
|
61
|
+
working_path = job.working_path
|
|
54
62
|
ImageSequenceManager.init(self, job)
|
|
55
63
|
if self.exif_path is None:
|
|
56
64
|
self.exif_path = job.action_path(0)
|
|
57
65
|
if self.exif_path != '':
|
|
58
|
-
self.exif_path = working_path
|
|
66
|
+
self.exif_path = os.path.join(working_path, self.exif_path)
|
|
59
67
|
|
|
60
68
|
|
|
61
69
|
def get_bunches(collection, n_frames, n_overlap):
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
def add_project_arguments(parser):
|
|
4
4
|
parser.add_argument('-x', '--expert', action='store_true', help='''
|
|
5
5
|
expert options are visible by default.
|
|
6
|
+
''')
|
|
7
|
+
parser.add_argument('-n', '--no-new-project', dest='new-project',
|
|
8
|
+
action='store_false', default=True, help='''
|
|
9
|
+
Do not open new project dialog at startup (default: open).
|
|
6
10
|
''')
|
|
7
11
|
|
|
8
12
|
|
shinestacker/app/gui_utils.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0116, E0611
|
|
1
|
+
# pylint: disable=C0114, C0116, E0611, R0913, R0917
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
from PySide6.QtCore import QCoreApplication, QProcess
|
|
@@ -6,6 +6,7 @@ from PySide6.QtGui import QAction
|
|
|
6
6
|
from shinestacker.config.constants import constants
|
|
7
7
|
from shinestacker.config.config import config
|
|
8
8
|
from shinestacker.app.about_dialog import show_about_dialog
|
|
9
|
+
from shinestacker.app.settings_dialog import show_settings_dialog
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def disable_macos_special_menu_items():
|
|
@@ -40,11 +41,18 @@ def disable_macos_special_menu_items():
|
|
|
40
41
|
QProcess.startDetached("pkill", ["-u", user, "-f", "SystemUIServer"])
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def fill_app_menu(app, app_menu
|
|
44
|
+
def fill_app_menu(app, app_menu, project_settings, retouch_settings,
|
|
45
|
+
handle_project_config, handle_retouch_config):
|
|
44
46
|
about_action = QAction(f"About {constants.APP_STRING}", app)
|
|
45
47
|
about_action.triggered.connect(lambda: show_about_dialog(app))
|
|
46
48
|
app_menu.addAction(about_action)
|
|
47
49
|
app_menu.addSeparator()
|
|
50
|
+
settings_action = QAction("Settings", app)
|
|
51
|
+
settings_action.triggered.connect(lambda: show_settings_dialog(
|
|
52
|
+
app, project_settings, retouch_settings,
|
|
53
|
+
handle_project_config, handle_retouch_config))
|
|
54
|
+
app_menu.addAction(settings_action)
|
|
55
|
+
app_menu.addSeparator()
|
|
48
56
|
if config.DONT_USE_NATIVE_MENU:
|
|
49
57
|
quit_txt, quit_short = "&Quit", "Ctrl+Q"
|
|
50
58
|
else:
|
shinestacker/app/main.py
CHANGED
|
@@ -13,6 +13,7 @@ from PySide6.QtCore import Qt, QEvent, QTimer, Signal
|
|
|
13
13
|
from shinestacker.config.config import config
|
|
14
14
|
config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
|
|
15
15
|
from shinestacker.config.constants import constants
|
|
16
|
+
from shinestacker.config.settings import StdPathFile
|
|
16
17
|
from shinestacker.core.logging import setup_logging
|
|
17
18
|
from shinestacker.gui.main_window import MainWindow
|
|
18
19
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
@@ -140,7 +141,9 @@ class MainApp(QMainWindow):
|
|
|
140
141
|
app_menu.addAction(self.switch_to_project_action)
|
|
141
142
|
app_menu.addAction(self.switch_to_retouch_action)
|
|
142
143
|
app_menu.addSeparator()
|
|
143
|
-
fill_app_menu(self, app_menu
|
|
144
|
+
fill_app_menu(self, app_menu, True, True,
|
|
145
|
+
self.project_window.handle_config,
|
|
146
|
+
self.retouch_window.handle_config)
|
|
144
147
|
return app_menu
|
|
145
148
|
|
|
146
149
|
def quit(self):
|
|
@@ -227,7 +230,8 @@ open retouch window at startup instead of project windows.
|
|
|
227
230
|
if filename and path:
|
|
228
231
|
print("can't specify both arguments --filename and --path", file=sys.stderr)
|
|
229
232
|
sys.exit(1)
|
|
230
|
-
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True
|
|
233
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
|
|
234
|
+
log_file=StdPathFile('shinestacker.log').get_file_path())
|
|
231
235
|
app = Application(sys.argv)
|
|
232
236
|
if config.DONT_USE_NATIVE_MENU:
|
|
233
237
|
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
|
@@ -264,7 +268,8 @@ open retouch window at startup instead of project windows.
|
|
|
264
268
|
main_app.switch_to_retouch()
|
|
265
269
|
else:
|
|
266
270
|
main_app.switch_to_project()
|
|
267
|
-
|
|
271
|
+
if args['new-project']:
|
|
272
|
+
QTimer.singleShot(100, main_app.project_window.project_controller.new_project)
|
|
268
273
|
QTimer.singleShot(100, main_app.setFocus)
|
|
269
274
|
sys.exit(app.exec())
|
|
270
275
|
|
shinestacker/app/project.py
CHANGED
|
@@ -12,6 +12,7 @@ from PySide6.QtCore import Qt, QTimer, QEvent
|
|
|
12
12
|
from shinestacker.config.config import config
|
|
13
13
|
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
14
14
|
from shinestacker.config.constants import constants
|
|
15
|
+
from shinestacker.config.settings import StdPathFile
|
|
15
16
|
from shinestacker.core.logging import setup_logging
|
|
16
17
|
from shinestacker.gui.main_window import MainWindow
|
|
17
18
|
from shinestacker.app.gui_utils import (
|
|
@@ -30,7 +31,9 @@ class ProjectApp(MainWindow):
|
|
|
30
31
|
|
|
31
32
|
def create_menu(self):
|
|
32
33
|
app_menu = QMenu(constants.APP_STRING)
|
|
33
|
-
fill_app_menu(self, app_menu
|
|
34
|
+
fill_app_menu(self, app_menu, True, False,
|
|
35
|
+
self.handle_config,
|
|
36
|
+
lambda: None)
|
|
34
37
|
return app_menu
|
|
35
38
|
|
|
36
39
|
def _retouch_callback(self, filename):
|
|
@@ -55,7 +58,8 @@ project filename.
|
|
|
55
58
|
''')
|
|
56
59
|
add_project_arguments(parser)
|
|
57
60
|
args = vars(parser.parse_args(sys.argv[1:]))
|
|
58
|
-
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True
|
|
61
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
|
|
62
|
+
log_file=StdPathFile('shinestacker.log').get_file_path())
|
|
59
63
|
app = Application(sys.argv)
|
|
60
64
|
if config.DONT_USE_NATIVE_MENU:
|
|
61
65
|
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
|
@@ -72,7 +76,7 @@ project filename.
|
|
|
72
76
|
filename = args['filename']
|
|
73
77
|
if filename:
|
|
74
78
|
QTimer.singleShot(100, lambda: window.project_controller.open_project(filename))
|
|
75
|
-
|
|
79
|
+
elif args['new-project']:
|
|
76
80
|
QTimer.singleShot(100, window.project_controller.new_project)
|
|
77
81
|
sys.exit(app.exec())
|
|
78
82
|
|
shinestacker/app/retouch.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
+
import logging
|
|
4
5
|
import argparse
|
|
5
6
|
from PySide6.QtWidgets import QApplication, QMenu
|
|
6
7
|
from PySide6.QtGui import QIcon
|
|
@@ -8,6 +9,8 @@ from PySide6.QtCore import Qt, QEvent
|
|
|
8
9
|
from shinestacker.config.config import config
|
|
9
10
|
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
10
11
|
from shinestacker.config.constants import constants
|
|
12
|
+
from shinestacker.config.settings import StdPathFile
|
|
13
|
+
from shinestacker.core.logging import setup_logging
|
|
11
14
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
12
15
|
from shinestacker.app.gui_utils import (
|
|
13
16
|
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
@@ -25,7 +28,9 @@ class RetouchApp(ImageEditorUI):
|
|
|
25
28
|
|
|
26
29
|
def create_menu(self):
|
|
27
30
|
app_menu = QMenu(constants.APP_STRING)
|
|
28
|
-
fill_app_menu(self, app_menu
|
|
31
|
+
fill_app_menu(self, app_menu, False, True,
|
|
32
|
+
lambda: None,
|
|
33
|
+
self.handle_config)
|
|
29
34
|
return app_menu
|
|
30
35
|
|
|
31
36
|
|
|
@@ -52,6 +57,8 @@ Multiple files can be specified separated by ';'.
|
|
|
52
57
|
if filename and path:
|
|
53
58
|
print("can't specify both arguments --filename and --path", file=sys.stderr)
|
|
54
59
|
sys.exit(1)
|
|
60
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
|
|
61
|
+
log_file=StdPathFile('shinestacker.log').get_file_path())
|
|
55
62
|
app = Application(sys.argv)
|
|
56
63
|
if config.DONT_USE_NATIVE_MENU:
|
|
57
64
|
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903, E0611, R0902
|
|
2
|
+
from PySide6.QtCore import Signal
|
|
3
|
+
from PySide6.QtWidgets import QFrame, QLabel, QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
|
|
4
|
+
from .. gui.config_dialog import ConfigDialog
|
|
5
|
+
from .. config.settings import Settings
|
|
6
|
+
from .. config.constants import constants
|
|
7
|
+
from .. config.gui_constants import gui_constants
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SettingsDialog(ConfigDialog):
|
|
11
|
+
update_project_config_requested = Signal()
|
|
12
|
+
update_retouch_config_requested = Signal()
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent=None, project_settings=True, retouch_settings=True):
|
|
15
|
+
self.project_settings = project_settings
|
|
16
|
+
self.retouch_settings = retouch_settings
|
|
17
|
+
self.settings = Settings.instance()
|
|
18
|
+
self.expert_options = None
|
|
19
|
+
self.combined_actions_max_threads = None
|
|
20
|
+
self.align_frames_max_threads = None
|
|
21
|
+
self.focus_stack_max_threads = None
|
|
22
|
+
self.view_strategy = None
|
|
23
|
+
self.min_mouse_step_brush_fraction = None
|
|
24
|
+
self.paint_refresh_time = None
|
|
25
|
+
self.display_refresh_time = None
|
|
26
|
+
self.cursor_update_time = None
|
|
27
|
+
super().__init__("Settings", parent)
|
|
28
|
+
|
|
29
|
+
def create_form_content(self):
|
|
30
|
+
if self.project_settings:
|
|
31
|
+
self.create_project_settings()
|
|
32
|
+
separator = QFrame()
|
|
33
|
+
separator.setFrameShape(QFrame.HLine)
|
|
34
|
+
separator.setFrameShadow(QFrame.Sunken)
|
|
35
|
+
separator.setLineWidth(1)
|
|
36
|
+
self.container_layout.addRow(separator)
|
|
37
|
+
if self.retouch_settings:
|
|
38
|
+
self.create_retouch_settings()
|
|
39
|
+
|
|
40
|
+
def create_project_settings(self):
|
|
41
|
+
label = QLabel("Project settings")
|
|
42
|
+
label.setStyleSheet("font-weight: bold")
|
|
43
|
+
self.container_layout.addRow(label)
|
|
44
|
+
self.expert_options = QCheckBox()
|
|
45
|
+
self.expert_options.setChecked(self.settings.get('expert_options'))
|
|
46
|
+
self.container_layout.addRow("Expert options:", self.expert_options)
|
|
47
|
+
self.combined_actions_max_threads = QSpinBox()
|
|
48
|
+
self.combined_actions_max_threads.setRange(0, 64)
|
|
49
|
+
self.combined_actions_max_threads.setValue(
|
|
50
|
+
self.settings.get('combined_actions_params')['max_threads'])
|
|
51
|
+
self.container_layout.addRow("Max num. of cores, combined actions:",
|
|
52
|
+
self.combined_actions_max_threads)
|
|
53
|
+
|
|
54
|
+
self.align_frames_max_threads = QSpinBox()
|
|
55
|
+
self.align_frames_max_threads.setRange(0, 64)
|
|
56
|
+
self.align_frames_max_threads.setValue(
|
|
57
|
+
self.settings.get('align_frames_params')['max_threads'])
|
|
58
|
+
self.container_layout.addRow("Max num. of cores, align frames:",
|
|
59
|
+
self.align_frames_max_threads)
|
|
60
|
+
|
|
61
|
+
self.focus_stack_max_threads = QSpinBox()
|
|
62
|
+
self.focus_stack_max_threads.setRange(0, 64)
|
|
63
|
+
self.focus_stack_max_threads.setValue(
|
|
64
|
+
self.settings.get('align_frames_params')['max_threads'])
|
|
65
|
+
self.container_layout.addRow("Max num. of cores, focus stacking:",
|
|
66
|
+
self.focus_stack_max_threads)
|
|
67
|
+
|
|
68
|
+
def create_retouch_settings(self):
|
|
69
|
+
label = QLabel("Retouch settings")
|
|
70
|
+
label.setStyleSheet("font-weight: bold")
|
|
71
|
+
self.container_layout.addRow(label)
|
|
72
|
+
self.view_strategy = QComboBox()
|
|
73
|
+
self.view_strategy.addItem("Overlaid", "overlaid")
|
|
74
|
+
self.view_strategy.addItem("Side by side", "sidebyside")
|
|
75
|
+
self.view_strategy.addItem("Top-Bottom", "topbottom")
|
|
76
|
+
idx = self.view_strategy.findData(self.settings.get('view_strategy'))
|
|
77
|
+
if idx >= 0:
|
|
78
|
+
self.view_strategy.setCurrentIndex(idx)
|
|
79
|
+
self.container_layout.addRow("View strategy:", self.view_strategy)
|
|
80
|
+
self.min_mouse_step_brush_fraction = QDoubleSpinBox()
|
|
81
|
+
self.min_mouse_step_brush_fraction.setValue(
|
|
82
|
+
self.settings.get('min_mouse_step_brush_fraction'))
|
|
83
|
+
self.min_mouse_step_brush_fraction.setRange(0, 1)
|
|
84
|
+
self.min_mouse_step_brush_fraction.setDecimals(2)
|
|
85
|
+
self.min_mouse_step_brush_fraction.setSingleStep(0.02)
|
|
86
|
+
self.container_layout.addRow("Min. mouse step in brush units:",
|
|
87
|
+
self.min_mouse_step_brush_fraction)
|
|
88
|
+
self.paint_refresh_time = QSpinBox()
|
|
89
|
+
self.paint_refresh_time.setRange(0, 1000)
|
|
90
|
+
self.paint_refresh_time.setValue(
|
|
91
|
+
self.settings.get('paint_refresh_time'))
|
|
92
|
+
self.container_layout.addRow("Paint refresh time:",
|
|
93
|
+
self.paint_refresh_time)
|
|
94
|
+
self.display_refresh_time = QSpinBox()
|
|
95
|
+
self.display_refresh_time.setRange(0, 200)
|
|
96
|
+
self.display_refresh_time.setValue(
|
|
97
|
+
self.settings.get('display_refresh_time'))
|
|
98
|
+
self.container_layout.addRow("Display refresh time:",
|
|
99
|
+
self.display_refresh_time)
|
|
100
|
+
|
|
101
|
+
self.cursor_update_time = QSpinBox()
|
|
102
|
+
self.cursor_update_time.setRange(0, 50)
|
|
103
|
+
self.cursor_update_time.setValue(
|
|
104
|
+
self.settings.get('cursor_update_time'))
|
|
105
|
+
self.container_layout.addRow("Cursor refresh time:",
|
|
106
|
+
self.cursor_update_time)
|
|
107
|
+
|
|
108
|
+
def accept(self):
|
|
109
|
+
if self.project_settings:
|
|
110
|
+
self.settings.set(
|
|
111
|
+
'expert_options', self.expert_options.isChecked())
|
|
112
|
+
self.settings.set(
|
|
113
|
+
'combined_actions_params', {
|
|
114
|
+
'max_threads': self.combined_actions_max_threads.value()
|
|
115
|
+
})
|
|
116
|
+
self.settings.set(
|
|
117
|
+
'align_frames_params', {
|
|
118
|
+
'max_threads': self.align_frames_max_threads.value()
|
|
119
|
+
})
|
|
120
|
+
self.settings.set(
|
|
121
|
+
'focus_stack_params', {
|
|
122
|
+
'max_threads': self.focus_stack_max_threads.value()
|
|
123
|
+
})
|
|
124
|
+
self.settings.set(
|
|
125
|
+
'focus_stack_bunch:params', {
|
|
126
|
+
'max_threads': self.focus_stack_max_threads.value()
|
|
127
|
+
})
|
|
128
|
+
if self.retouch_settings:
|
|
129
|
+
self.settings.set(
|
|
130
|
+
'view_strategy', self.view_strategy.itemData(self.view_strategy.currentIndex()))
|
|
131
|
+
self.settings.set(
|
|
132
|
+
'min_mouse_step_brush_fraction', self.min_mouse_step_brush_fraction.value())
|
|
133
|
+
self.settings.set(
|
|
134
|
+
'paint_refresh_time', self.paint_refresh_time.value())
|
|
135
|
+
self.settings.set(
|
|
136
|
+
'display_refresh_time', self.display_refresh_time.value())
|
|
137
|
+
self.settings.set(
|
|
138
|
+
'cursor_update_time', self.cursor_update_time.value())
|
|
139
|
+
self.settings.update()
|
|
140
|
+
if self.project_settings:
|
|
141
|
+
self.update_project_config_requested.emit()
|
|
142
|
+
if self.retouch_settings:
|
|
143
|
+
self.update_retouch_config_requested.emit()
|
|
144
|
+
super().accept()
|
|
145
|
+
|
|
146
|
+
def reset_to_defaults(self):
|
|
147
|
+
if self.project_settings:
|
|
148
|
+
self.expert_options.setChecked(constants.DEFAULT_EXPERT_OPTIONS)
|
|
149
|
+
self.combined_actions_max_threads.setValue(constants.DEFAULT_MAX_FWK_THREADS)
|
|
150
|
+
self.align_frames_max_threads.setValue(constants.DEFAULT_ALIGN_MAX_THREADS)
|
|
151
|
+
self.focus_stack_max_threads.setValue(constants.DEFAULT_PY_MAX_THREADS)
|
|
152
|
+
if self.retouch_settings:
|
|
153
|
+
idx = self.view_strategy.findData(constants.DEFAULT_VIEW_STRATEGY)
|
|
154
|
+
if idx >= 0:
|
|
155
|
+
self.view_strategy.setCurrentIndex(idx)
|
|
156
|
+
self.min_mouse_step_brush_fraction.setValue(
|
|
157
|
+
gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION)
|
|
158
|
+
self.paint_refresh_time.setValue(
|
|
159
|
+
gui_constants.DEFAULT_PAINT_REFRESH_TIME)
|
|
160
|
+
self.display_refresh_time.setValue(
|
|
161
|
+
gui_constants.DEFAULT_DISPLAY_REFRESH_TIME)
|
|
162
|
+
self.cursor_update_time.setValue(
|
|
163
|
+
gui_constants.DEFAULT_CURSOR_UPDATE_TIME)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def show_settings_dialog(parent, project_settings, retouch_settings,
|
|
167
|
+
handle_project_config, handle_retouch_config):
|
|
168
|
+
dialog = SettingsDialog(parent, project_settings, retouch_settings)
|
|
169
|
+
dialog.update_project_config_requested.connect(handle_project_config)
|
|
170
|
+
dialog.update_retouch_config_requested.connect(handle_retouch_config)
|
|
171
|
+
dialog.exec()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116
|
|
2
|
+
from .settings import Settings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AppConfig:
|
|
6
|
+
_instance = None
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
if AppConfig._instance is not None:
|
|
10
|
+
raise RuntimeError("AppConfig is a singleton.")
|
|
11
|
+
self.config = {}
|
|
12
|
+
Settings.add_observer(self)
|
|
13
|
+
self.update(Settings.instance().settings)
|
|
14
|
+
|
|
15
|
+
def update(self, settings):
|
|
16
|
+
self.config = {**self.config, **settings}
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def instance(cls):
|
|
20
|
+
if cls._instance is None:
|
|
21
|
+
cls._instance = cls()
|
|
22
|
+
return cls._instance
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get(cls, key, default=None):
|
|
26
|
+
return cls.instance().config.get(key, default)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def set(cls, key, value):
|
|
30
|
+
cls.instance().config[key] = value
|
shinestacker/config/constants.py
CHANGED
|
@@ -236,6 +236,9 @@ class _Constants:
|
|
|
236
236
|
DEFAULT_PLOT_STACK_BUNCH = True
|
|
237
237
|
DEFAULT_PLOT_STACK = True
|
|
238
238
|
|
|
239
|
+
DEFAULT_EXPERT_OPTIONS = False
|
|
240
|
+
DEFAULT_VIEW_STRATEGY = 'overlaid'
|
|
241
|
+
|
|
239
242
|
def __setattr__aux(self, name, value):
|
|
240
243
|
raise AttributeError(f"Can't reassign constant '{name}'")
|
|
241
244
|
|
|
@@ -32,8 +32,10 @@ class _GuiConstants:
|
|
|
32
32
|
'preview_inner': (255, 255, 255, 150)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION = 0.25
|
|
36
|
+
DEFAULT_PAINT_REFRESH_TIME = 50 # ms
|
|
37
|
+
DEFAULT_DISPLAY_REFRESH_TIME = 50 # ms
|
|
38
|
+
DEFAULT_CURSOR_UPDATE_TIME = 16 # ms
|
|
37
39
|
|
|
38
40
|
THUMB_WIDTH = 120 # px
|
|
39
41
|
THUMB_HEIGHT = 80 # px
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903, E0611
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import traceback
|
|
5
|
+
import jsonpickle
|
|
6
|
+
from PySide6.QtCore import QStandardPaths
|
|
7
|
+
from .. config.constants import constants
|
|
8
|
+
from .. config.gui_constants import gui_constants
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StdPathFile:
|
|
12
|
+
def __init__(self, filename):
|
|
13
|
+
self._config_dir = None
|
|
14
|
+
self.filename = filename
|
|
15
|
+
|
|
16
|
+
def get_config_dir(self):
|
|
17
|
+
if self._config_dir is None:
|
|
18
|
+
config_dir = QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)
|
|
19
|
+
if not config_dir:
|
|
20
|
+
if os.name == 'nt': # Windows
|
|
21
|
+
config_dir = os.path.join(os.environ.get('APPDATA', ''), 'ShineStacker')
|
|
22
|
+
elif os.name == 'posix': # macOS and Linux
|
|
23
|
+
config_dir = os.path.expanduser('~/.config/shinestacker')
|
|
24
|
+
else:
|
|
25
|
+
config_dir = os.path.join(os.path.expanduser('~'), '.shinestacker')
|
|
26
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
27
|
+
self._config_dir = config_dir
|
|
28
|
+
return self._config_dir
|
|
29
|
+
|
|
30
|
+
def get_file_path(self):
|
|
31
|
+
return os.path.join(self.get_config_dir(), self.filename)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT_SETTINGS = {
|
|
35
|
+
'expert_options': constants.DEFAULT_EXPERT_OPTIONS,
|
|
36
|
+
'view_strategy': constants.DEFAULT_VIEW_STRATEGY,
|
|
37
|
+
'paint_refresh_time': gui_constants.DEFAULT_PAINT_REFRESH_TIME,
|
|
38
|
+
'display_refresh_time': gui_constants.DEFAULT_DISPLAY_REFRESH_TIME,
|
|
39
|
+
'cursor_update_time': gui_constants.DEFAULT_CURSOR_UPDATE_TIME,
|
|
40
|
+
'min_mouse_step_brush_fraction': gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION,
|
|
41
|
+
'combined_actions_params': {
|
|
42
|
+
'max_threads': constants.DEFAULT_MAX_FWK_THREADS
|
|
43
|
+
},
|
|
44
|
+
'align_frames_params': {
|
|
45
|
+
'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS
|
|
46
|
+
},
|
|
47
|
+
'focus_stack_params': {
|
|
48
|
+
'max_threads': constants.DEFAULT_PY_MAX_THREADS
|
|
49
|
+
},
|
|
50
|
+
'focus_stack_bunch_params': {
|
|
51
|
+
'max_threads': constants.DEFAULT_PY_MAX_THREADS
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
CURRENT_VERSION = 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Settings(StdPathFile):
|
|
59
|
+
_instance = None
|
|
60
|
+
_observers = []
|
|
61
|
+
|
|
62
|
+
def __init__(self, filename):
|
|
63
|
+
if Settings._instance is not None:
|
|
64
|
+
raise RuntimeError("Settings is a singleton.")
|
|
65
|
+
super().__init__(filename)
|
|
66
|
+
self.settings = DEFAULT_SETTINGS
|
|
67
|
+
file_path = self.get_file_path()
|
|
68
|
+
if os.path.isfile(file_path):
|
|
69
|
+
try:
|
|
70
|
+
with open(file_path, 'r', encoding="utf-8") as file:
|
|
71
|
+
json_data = json.load(file)
|
|
72
|
+
settings = json_data['settings']
|
|
73
|
+
except Exception as e:
|
|
74
|
+
traceback.print_tb(e.__traceback__)
|
|
75
|
+
print(f"Can't read file from path {file_path}. Default settings ignored.")
|
|
76
|
+
settings = {}
|
|
77
|
+
self.settings = {**self.settings, **settings}
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def instance(cls, filename="shinestacker-settings.txt"):
|
|
81
|
+
if cls._instance is None:
|
|
82
|
+
cls._instance = cls(filename)
|
|
83
|
+
return cls._instance
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def add_observer(cls, observer):
|
|
87
|
+
cls._observers.append(observer)
|
|
88
|
+
|
|
89
|
+
def set(self, key, value):
|
|
90
|
+
self.settings[key] = value
|
|
91
|
+
|
|
92
|
+
def get(self, key, default=None):
|
|
93
|
+
return self.settings.get(key, default)
|
|
94
|
+
|
|
95
|
+
def update(self):
|
|
96
|
+
try:
|
|
97
|
+
config_dir = self.get_config_dir()
|
|
98
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
99
|
+
json_data = {'version': CURRENT_VERSION, 'settings': self.settings}
|
|
100
|
+
json_obj = jsonpickle.encode(json_data)
|
|
101
|
+
with open(self.get_file_path(), 'w', encoding="utf-8") as f:
|
|
102
|
+
f.write(json_obj)
|
|
103
|
+
except IOError as e:
|
|
104
|
+
raise e
|
|
105
|
+
for observer in Settings._observers:
|
|
106
|
+
observer.update(self.settings)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def reset_instance_only_for_testing(cls):
|
|
110
|
+
cls._instance = None
|
shinestacker/core/core_utils.py
CHANGED
|
@@ -26,19 +26,10 @@ def make_tqdm_bar(name, size, ncols=80):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def get_app_base_path():
|
|
29
|
-
sep = '\\' if (platform.system() == 'Windows') else '/'
|
|
30
29
|
if getattr(sys, 'frozen', False):
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for i in range(len(dirs) - 1, -1, -1):
|
|
35
|
-
if dirs[i] == 'shinestacker':
|
|
36
|
-
last = i
|
|
37
|
-
break
|
|
38
|
-
path = sep.join(dirs if last == 1 else dirs[:last + 1])
|
|
39
|
-
elif __file__:
|
|
40
|
-
path = sep.join(os.path.dirname(os.path.abspath(__file__)).split(sep)[:-3])
|
|
41
|
-
return path
|
|
30
|
+
return os.path.dirname(os.path.abspath(sys.executable))
|
|
31
|
+
else:
|
|
32
|
+
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
42
33
|
|
|
43
34
|
|
|
44
35
|
def running_under_windows() -> bool:
|
shinestacker/core/logging.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116
|
|
2
|
+
import os
|
|
2
3
|
import logging
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
@@ -64,8 +65,8 @@ def setup_logging(console_level=logging.INFO, file_level=logging.DEBUG, log_file
|
|
|
64
65
|
if log_file == '':
|
|
65
66
|
today = datetime.date.today().strftime("%Y-%m-%d")
|
|
66
67
|
log_file = f"logs/{constants.APP_STRING.lower()}-{today}.log"
|
|
67
|
-
if log_file
|
|
68
|
-
log_file =
|
|
68
|
+
if not os.path.isabs(log_file):
|
|
69
|
+
log_file = os.path.join(get_app_base_path(), {log_file})
|
|
69
70
|
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
|
|
70
71
|
file_handler = logging.FileHandler(log_file)
|
|
71
72
|
file_handler.setLevel(file_level)
|
|
@@ -49,7 +49,7 @@ class FieldBuilder:
|
|
|
49
49
|
self.fields = {}
|
|
50
50
|
|
|
51
51
|
def add_field(self, tag, field_type, label,
|
|
52
|
-
required=False, add_to_layout=None, **kwargs):
|
|
52
|
+
required=False, add_to_layout=None, do_add=True, **kwargs):
|
|
53
53
|
if field_type == FIELD_TEXT:
|
|
54
54
|
widget = self.create_text_field(tag, **kwargs)
|
|
55
55
|
elif field_type == FIELD_ABS_PATH:
|
|
@@ -99,9 +99,10 @@ class FieldBuilder:
|
|
|
99
99
|
'default_value': default_value,
|
|
100
100
|
**kwargs
|
|
101
101
|
}
|
|
102
|
-
if
|
|
103
|
-
add_to_layout
|
|
104
|
-
|
|
102
|
+
if do_add:
|
|
103
|
+
if add_to_layout is None:
|
|
104
|
+
add_to_layout = self.main_layout
|
|
105
|
+
add_to_layout.addRow(f"{label}:", widget)
|
|
105
106
|
return widget
|
|
106
107
|
|
|
107
108
|
def reset_to_defaults(self):
|
|
@@ -499,7 +500,7 @@ class DefaultActionConfigurator(NoNameActionConfigurator):
|
|
|
499
500
|
name_row = QHBoxLayout()
|
|
500
501
|
name_row.setContentsMargins(0, 0, 0, 0)
|
|
501
502
|
name_label = QLabel(f"{tag} name:")
|
|
502
|
-
name_field = self.
|
|
503
|
+
name_field = self.add_field('name', FIELD_TEXT, f"{tag} name", required=False, do_add=False)
|
|
503
504
|
name_row.addWidget(name_label)
|
|
504
505
|
name_row.addWidget(name_field, 1)
|
|
505
506
|
name_row.addStretch()
|