shinestacker 0.5.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +4 -12
- shinestacker/algorithms/balance.py +11 -9
- shinestacker/algorithms/depth_map.py +0 -30
- shinestacker/algorithms/utils.py +10 -0
- shinestacker/algorithms/vignetting.py +116 -70
- shinestacker/app/about_dialog.py +37 -16
- shinestacker/app/gui_utils.py +1 -1
- shinestacker/app/help_menu.py +1 -1
- shinestacker/app/main.py +2 -2
- shinestacker/app/project.py +2 -2
- shinestacker/config/constants.py +4 -1
- shinestacker/config/gui_constants.py +3 -4
- shinestacker/gui/action_config.py +5 -561
- shinestacker/gui/action_config_dialog.py +567 -0
- shinestacker/gui/base_form_dialog.py +18 -0
- shinestacker/gui/colors.py +5 -6
- shinestacker/gui/gui_logging.py +0 -1
- shinestacker/gui/gui_run.py +54 -106
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker/gui/ico/shinestacker.svg +60 -0
- shinestacker/gui/main_window.py +275 -371
- shinestacker/gui/menu_manager.py +236 -0
- shinestacker/gui/new_project.py +75 -20
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +248 -165
- shinestacker/gui/project_model.py +2 -7
- shinestacker/gui/tab_widget.py +81 -0
- shinestacker/gui/time_progress_bar.py +95 -0
- shinestacker/retouch/base_filter.py +173 -40
- shinestacker/retouch/brush_preview.py +0 -10
- shinestacker/retouch/brush_tool.py +2 -5
- shinestacker/retouch/denoise_filter.py +5 -44
- shinestacker/retouch/exif_data.py +10 -13
- shinestacker/retouch/file_loader.py +1 -1
- shinestacker/retouch/filter_manager.py +1 -4
- shinestacker/retouch/image_editor_ui.py +318 -40
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +34 -30
- shinestacker/retouch/layer_collection.py +2 -0
- shinestacker/retouch/shortcuts_help.py +12 -0
- shinestacker/retouch/unsharp_mask_filter.py +10 -10
- shinestacker/retouch/vignetting_filter.py +69 -0
- shinestacker/retouch/white_balance_filter.py +46 -14
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
- shinestacker-1.0.0.dist-info/RECORD +90 -0
- shinestacker/app/app_config.py +0 -22
- shinestacker/gui/actions_window.py +0 -266
- shinestacker/retouch/image_editor.py +0 -197
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.5.0.dist-info/RECORD +0 -87
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -6,8 +6,8 @@ from .base_filter import BaseFilter
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class UnsharpMaskFilter(BaseFilter):
|
|
9
|
-
def __init__(self, editor):
|
|
10
|
-
super().__init__(editor)
|
|
9
|
+
def __init__(self, name, editor):
|
|
10
|
+
super().__init__(name, editor, preview_at_startup=True)
|
|
11
11
|
self.max_range = 500.0
|
|
12
12
|
self.max_radius = 4.0
|
|
13
13
|
self.max_amount = 3.0
|
|
@@ -46,14 +46,14 @@ class UnsharpMaskFilter(BaseFilter):
|
|
|
46
46
|
elif name == "Threshold":
|
|
47
47
|
self.threshold_slider = slider
|
|
48
48
|
value_labels[name] = value_label
|
|
49
|
-
|
|
50
|
-
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
|
|
49
|
+
self.create_base_widgets(
|
|
50
|
+
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
|
|
51
51
|
|
|
52
52
|
def update_value(name, value, max_val, fmt):
|
|
53
53
|
float_value = max_val * value / self.max_range
|
|
54
54
|
value_labels[name].setText(fmt.format(float_value))
|
|
55
|
-
if preview_check.isChecked():
|
|
56
|
-
preview_timer.start()
|
|
55
|
+
if self.preview_check.isChecked():
|
|
56
|
+
self.preview_timer.start()
|
|
57
57
|
|
|
58
58
|
self.radius_slider.valueChanged.connect(
|
|
59
59
|
lambda v: update_value("Radius", v, self.max_radius, params["Radius"][2]))
|
|
@@ -61,10 +61,10 @@ class UnsharpMaskFilter(BaseFilter):
|
|
|
61
61
|
lambda v: update_value("Amount", v, self.max_amount, params["Amount"][2]))
|
|
62
62
|
self.threshold_slider.valueChanged.connect(
|
|
63
63
|
lambda v: update_value("Threshold", v, self.max_threshold, params["Threshold"][2]))
|
|
64
|
-
preview_timer.timeout.connect(do_preview)
|
|
65
|
-
self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
66
|
-
button_box.accepted.connect(dlg.accept)
|
|
67
|
-
button_box.rejected.connect(dlg.reject)
|
|
64
|
+
self.preview_timer.timeout.connect(do_preview)
|
|
65
|
+
self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
|
|
66
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
67
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
68
68
|
QTimer.singleShot(0, do_preview)
|
|
69
69
|
|
|
70
70
|
def get_params(self):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902
|
|
2
|
+
from PySide6.QtCore import Qt
|
|
3
|
+
from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider
|
|
4
|
+
from .. config.constants import constants
|
|
5
|
+
from .. algorithms.vignetting import correct_vignetting
|
|
6
|
+
from .base_filter import OneSliderBaseFilter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VignettingFilter(OneSliderBaseFilter):
|
|
10
|
+
def __init__(self, name, editor):
|
|
11
|
+
super().__init__(name, editor, 1.0, 0.90, "Vignetting correction",
|
|
12
|
+
allow_partial_preview=False, preview_at_startup=False)
|
|
13
|
+
self.subsample_box = None
|
|
14
|
+
self.fast_subsampling_check = None
|
|
15
|
+
self.r_steps_box = None
|
|
16
|
+
self.threshold_slider = None
|
|
17
|
+
self.threshold_label = None
|
|
18
|
+
self.threshold_max_range = 500
|
|
19
|
+
self.threshold_max_value = 128.0
|
|
20
|
+
self.threshold_initial_value = constants.DEFAULT_BLACK_THRESHOLD
|
|
21
|
+
self.threshold_format = "{:.1f}"
|
|
22
|
+
|
|
23
|
+
def apply(self, image, strength):
|
|
24
|
+
return correct_vignetting(image, max_correction=strength,
|
|
25
|
+
black_threshold=self.threshold_slider.value(),
|
|
26
|
+
r_steps=self.r_steps_box.value(),
|
|
27
|
+
subsample=self.subsample_box.value(),
|
|
28
|
+
fast_subsampling=True)
|
|
29
|
+
|
|
30
|
+
def add_widgets(self, layout, dlg):
|
|
31
|
+
threshold_layout = QHBoxLayout()
|
|
32
|
+
threshold_layout.addWidget(QLabel("Threshold:"))
|
|
33
|
+
self.threshold_slider = QSlider(Qt.Horizontal)
|
|
34
|
+
self.threshold_slider.setRange(0, self.threshold_max_range)
|
|
35
|
+
self.threshold_slider.setValue(
|
|
36
|
+
int(self.threshold_initial_value /
|
|
37
|
+
self.threshold_max_value * self.threshold_max_range))
|
|
38
|
+
self.threshold_slider.valueChanged.connect(self.threshold_changed)
|
|
39
|
+
self.threshold_label = QLabel(self.threshold_format.format(self.threshold_initial_value))
|
|
40
|
+
threshold_layout.addWidget(self.threshold_slider)
|
|
41
|
+
threshold_layout.addWidget(self.threshold_label)
|
|
42
|
+
layout.addLayout(threshold_layout)
|
|
43
|
+
subsample_layout = QHBoxLayout()
|
|
44
|
+
subsample_label = QLabel("Subsample:")
|
|
45
|
+
self.subsample_box = QSpinBox()
|
|
46
|
+
self.subsample_box.setFixedWidth(50)
|
|
47
|
+
self.subsample_box.setRange(1, 50)
|
|
48
|
+
self.subsample_box.setValue(constants.DEFAULT_VIGN_SUBSAMPLE)
|
|
49
|
+
self.subsample_box.valueChanged.connect(self.threshold_changed)
|
|
50
|
+
self.fast_subsampling_check = QCheckBox("Fast subsampling")
|
|
51
|
+
self.fast_subsampling_check.setChecked(constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
|
|
52
|
+
r_steps_label = QLabel("Radial steps:")
|
|
53
|
+
self.r_steps_box = QSpinBox()
|
|
54
|
+
self.r_steps_box.setFixedWidth(50)
|
|
55
|
+
self.r_steps_box.setRange(1, 200)
|
|
56
|
+
self.r_steps_box.setValue(constants.DEFAULT_R_STEPS)
|
|
57
|
+
self.r_steps_box.valueChanged.connect(self.param_changed)
|
|
58
|
+
subsample_layout.addWidget(subsample_label)
|
|
59
|
+
subsample_layout.addWidget(self.subsample_box)
|
|
60
|
+
subsample_layout.addWidget(r_steps_label)
|
|
61
|
+
subsample_layout.addWidget(self.r_steps_box)
|
|
62
|
+
subsample_layout.addStretch(1)
|
|
63
|
+
layout.addLayout(subsample_layout)
|
|
64
|
+
layout.addWidget(self.fast_subsampling_check)
|
|
65
|
+
|
|
66
|
+
def threshold_changed(self, val):
|
|
67
|
+
float_val = self.threshold_max_value * float(val) / self.threshold_max_range
|
|
68
|
+
self.threshold_label.setText(self.threshold_format.format(float_val))
|
|
69
|
+
self.param_changed(val)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917, R0902
|
|
2
2
|
from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QFrame, QVBoxLayout, QLabel, QDialog,
|
|
3
|
-
QApplication, QSlider, QDialogButtonBox)
|
|
3
|
+
QApplication, QSlider, QDialogButtonBox, QLineEdit)
|
|
4
4
|
from PySide6.QtCore import Qt, QTimer
|
|
5
5
|
from PySide6.QtGui import QCursor
|
|
6
6
|
from .. algorithms.white_balance import white_balance_from_rgb
|
|
@@ -8,12 +8,13 @@ from .base_filter import BaseFilter
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class WhiteBalanceFilter(BaseFilter):
|
|
11
|
-
def __init__(self, editor):
|
|
12
|
-
super().__init__(editor)
|
|
11
|
+
def __init__(self, name, editor):
|
|
12
|
+
super().__init__(name, editor, preview_at_startup=True)
|
|
13
13
|
self.max_range = 255
|
|
14
14
|
self.initial_val = (128, 128, 128)
|
|
15
15
|
self.sliders = {}
|
|
16
16
|
self.value_labels = {}
|
|
17
|
+
self.rgb_hex = None
|
|
17
18
|
self.color_preview = None
|
|
18
19
|
self.preview_timer = None
|
|
19
20
|
self.original_mouse_press = None
|
|
@@ -45,29 +46,60 @@ class WhiteBalanceFilter(BaseFilter):
|
|
|
45
46
|
self.value_labels[name] = val_label
|
|
46
47
|
row_layout.addLayout(sliders_layout)
|
|
47
48
|
layout.addLayout(row_layout)
|
|
49
|
+
|
|
50
|
+
rbg_layout = QHBoxLayout()
|
|
51
|
+
rbg_layout.addWidget(QLabel("RBG hex:"))
|
|
52
|
+
self.rgb_hex = QLineEdit(self.hex_color(self.initial_val))
|
|
53
|
+
self.rgb_hex.setFixedWidth(60)
|
|
54
|
+
self.rgb_hex.textChanged.connect(self.on_rgb_change)
|
|
55
|
+
rbg_layout.addWidget(self.rgb_hex)
|
|
56
|
+
rbg_layout.addStretch(1)
|
|
57
|
+
layout.addLayout(rbg_layout)
|
|
58
|
+
|
|
48
59
|
pick_button = QPushButton("Pick Color")
|
|
49
60
|
layout.addWidget(pick_button)
|
|
50
|
-
|
|
61
|
+
self.create_base_widgets(
|
|
51
62
|
layout,
|
|
52
63
|
QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel,
|
|
53
|
-
200)
|
|
64
|
+
200, dlg)
|
|
54
65
|
for slider in self.sliders.values():
|
|
55
66
|
slider.valueChanged.connect(self.on_slider_change)
|
|
56
67
|
self.preview_timer.timeout.connect(do_preview)
|
|
57
|
-
self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
68
|
+
self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
|
|
58
69
|
pick_button.clicked.connect(self.start_color_pick)
|
|
59
|
-
button_box.accepted.connect(dlg.accept)
|
|
60
|
-
button_box.rejected.connect(dlg.reject)
|
|
61
|
-
button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
|
|
70
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
71
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
72
|
+
self.button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
|
|
62
73
|
QTimer.singleShot(0, do_preview)
|
|
63
74
|
|
|
75
|
+
def hex_color(self, val):
|
|
76
|
+
return "".join([f"{int(c):0>2X}" for c in val])
|
|
77
|
+
|
|
78
|
+
def apply_preview(self, rgb):
|
|
79
|
+
self.color_preview.setStyleSheet(f"background-color: rgb{tuple(rgb)};")
|
|
80
|
+
if self.preview_timer:
|
|
81
|
+
self.preview_timer.start()
|
|
82
|
+
|
|
64
83
|
def on_slider_change(self):
|
|
65
84
|
for name in ("R", "G", "B"):
|
|
66
85
|
self.value_labels[name].setText(str(self.sliders[name].value()))
|
|
67
86
|
rgb = tuple(self.sliders[n].value() for n in ("R", "G", "B"))
|
|
68
|
-
self.
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
self.rgb_hex.blockSignals(True)
|
|
88
|
+
self.rgb_hex.setText(self.hex_color(rgb))
|
|
89
|
+
self.rgb_hex.blockSignals(False)
|
|
90
|
+
self.apply_preview(rgb)
|
|
91
|
+
|
|
92
|
+
def on_rgb_change(self):
|
|
93
|
+
txt = self.rgb_hex.text()
|
|
94
|
+
if len(txt) != 6:
|
|
95
|
+
return
|
|
96
|
+
rgb = [int(txt[i:i + 2], 16) for i in range(0, 6, 2)]
|
|
97
|
+
for name, c in zip(("R", "G", "B"), rgb):
|
|
98
|
+
self.sliders[name].blockSignals(True)
|
|
99
|
+
self.sliders[name].setValue(c)
|
|
100
|
+
self.sliders[name].blockSignals(False)
|
|
101
|
+
self.value_labels[name].setText(str(c))
|
|
102
|
+
self.apply_preview(rgb)
|
|
71
103
|
|
|
72
104
|
def start_color_pick(self):
|
|
73
105
|
for widget in QApplication.topLevelWidgets():
|
|
@@ -94,7 +126,7 @@ class WhiteBalanceFilter(BaseFilter):
|
|
|
94
126
|
self.editor.image_viewer.mousePressEvent = self.original_mouse_press
|
|
95
127
|
self.editor.image_viewer.brush_cursor.show()
|
|
96
128
|
self.editor.brush_preview.show()
|
|
97
|
-
new_filter = WhiteBalanceFilter(self.editor)
|
|
129
|
+
new_filter = WhiteBalanceFilter(self.name, self.editor)
|
|
98
130
|
new_filter.run_with_preview(init_val=rgb)
|
|
99
131
|
|
|
100
132
|
def reset_rgb(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -41,6 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
[](https://codecov.io/github/lucalista/shinestacker)
|
|
42
42
|
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
44
|
+
[](https://pepy.tech/projects/shinestacker)
|
|
44
45
|
|
|
45
46
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
|
|
46
47
|
|
|
@@ -75,6 +76,14 @@ The GUI has two main working areas:
|
|
|
75
76
|
|
|
76
77
|
The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
|
|
77
78
|
|
|
79
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="256" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
|
|
80
|
+
|
|
81
|
+
## Logo attribution
|
|
82
|
+
|
|
83
|
+
The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
84
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
85
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
86
|
+
|
|
78
87
|
# Resources
|
|
79
88
|
|
|
80
89
|
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
@@ -83,8 +92,11 @@ Pyramid methods in image processing
|
|
|
83
92
|
|
|
84
93
|
# License
|
|
85
94
|
|
|
86
|
-
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
95
|
+
- **Code**: <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
87
96
|
The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
97
|
+
- **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
98
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
99
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
88
100
|
|
|
89
101
|
# Attribution request
|
|
90
102
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
+
shinestacker/_version.py,sha256=dHuoY6voK7np1A7oRzw1xyBy7CK9_KBAc4FCKW29uRQ,21
|
|
3
|
+
shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
|
|
4
|
+
shinestacker/algorithms/align.py,sha256=XT4DJoD5ZvpkC1-J3W3GWmWRsXJg3qJ-3zr9erT8oW0,17514
|
|
5
|
+
shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
|
|
6
|
+
shinestacker/algorithms/base_stack_algo.py,sha256=AFV2QkcFNaTcnISpsWHuAVy2De9hhaPcBNjE1O0h50I,1430
|
|
7
|
+
shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
|
|
8
|
+
shinestacker/algorithms/depth_map.py,sha256=FOR5M0brO5-9NnXDY7TWpc3OtKKSuzrOSoBMe0cP6Ho,6076
|
|
9
|
+
shinestacker/algorithms/exif.py,sha256=gY9s6Cd4g4swo5qEjSbzuVIvl1GImCYu6ytOO9WrV0I,9435
|
|
10
|
+
shinestacker/algorithms/multilayer.py,sha256=5JA6TW8oO_R3mu6cOvPno9by4md8q5sXUb8ZfsRRpmY,9259
|
|
11
|
+
shinestacker/algorithms/noise_detection.py,sha256=CDnN8pglxufY5Y-dT3mVooD4zPySdSq9CMgtDGMXBnA,8970
|
|
12
|
+
shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
|
|
13
|
+
shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
|
|
14
|
+
shinestacker/algorithms/stack.py,sha256=FCU89Of-s6C_DuMleG06c8V6fnIm9MFInvkkKtTsGBo,4906
|
|
15
|
+
shinestacker/algorithms/stack_framework.py,sha256=frw7sbc9qOfVBYP3ZOFZEaIn9O27Wms8j_mxSW79uI0,12460
|
|
16
|
+
shinestacker/algorithms/utils.py,sha256=2XFa16Q8JVq4C2iXZFOvv98GVKqxceFG73yFZNHJSqI,2475
|
|
17
|
+
shinestacker/algorithms/vignetting.py,sha256=yW-1TF4tesLWfKQOS0XxRkOEN82U-YDmMaj09C9cH4M,9552
|
|
18
|
+
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
19
|
+
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
|
|
21
|
+
shinestacker/app/gui_utils.py,sha256=08TrCj2gFGsNsF6hG7ySO2y7wcQakM5PzERkeplqNFs,2344
|
|
22
|
+
shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
|
|
23
|
+
shinestacker/app/main.py,sha256=geZwxShTDg8dDOnsGu9TvWMw2Wru6MaX9CvlGHMnRvQ,6474
|
|
24
|
+
shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
|
|
25
|
+
shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2774
|
|
26
|
+
shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
|
|
27
|
+
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
28
|
+
shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
|
|
29
|
+
shinestacker/config/constants.py,sha256=l7RcRxXlIThkBXcy2GVRBWFZQRXNgqYShNEVfDpgjEU,6096
|
|
30
|
+
shinestacker/config/gui_constants.py,sha256=5DR-ET1oeMMD7lIsjvAwSuln89A7I9wy9VuAeRo2G64,2575
|
|
31
|
+
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
32
|
+
shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
|
|
33
|
+
shinestacker/core/core_utils.py,sha256=ulJhzen5McAb5n6wWNA_KB4U_PdTEr-H2TCQkVKUaOw,1421
|
|
34
|
+
shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
|
|
35
|
+
shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7Kk,7447
|
|
36
|
+
shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
|
|
37
|
+
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
shinestacker/gui/action_config.py,sha256=yXNDv0MyONbHk4iUrkvMkLKKaDvpJyzA5Yr0Eikgo0c,16986
|
|
39
|
+
shinestacker/gui/action_config_dialog.py,sha256=6Xjbtj7oHGXBNiogcnPoFHqcuTOnZFlMzWCZXv8eBAI,32623
|
|
40
|
+
shinestacker/gui/base_form_dialog.py,sha256=yYqMee1mzw9VBx8siBS0jDk1qqsTIKJUgdjh92aprQk,687
|
|
41
|
+
shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
|
|
42
|
+
shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
|
|
43
|
+
shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
|
|
44
|
+
shinestacker/gui/gui_run.py,sha256=jGrtXNT3Gn53qMPSU74aEY6PP7UQfc85SD3XIesWLuY,15147
|
|
45
|
+
shinestacker/gui/main_window.py,sha256=6zGFZkgBh3_UNWjCfNzciPeiCvBsZnjJiPy62U4ldAw,23988
|
|
46
|
+
shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
|
|
47
|
+
shinestacker/gui/new_project.py,sha256=zHmGrT27L7I6YHM1L8wjt7DzukLFPddFsbVyGVHfJoc,11004
|
|
48
|
+
shinestacker/gui/project_converter.py,sha256=_AFfU2HYKPX78l6iX6bXJrlKpdjSl63pmKzrc6kQpn8,7348
|
|
49
|
+
shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
|
|
50
|
+
shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
|
|
51
|
+
shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
|
|
52
|
+
shinestacker/gui/tab_widget.py,sha256=6iUifK-wu0EzjVFccKHirhA2fENglVi6xREKiD96aaY,2950
|
|
53
|
+
shinestacker/gui/time_progress_bar.py,sha256=Ty7pNTfbKU44Y_0YQNYtgEcxpOD-Bbi4lC8g-u9bno0,3012
|
|
54
|
+
shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
|
|
55
|
+
shinestacker/gui/ico/shinestacker.icns,sha256=3IshIOv0uFexYsAEPkE9xiyuw8mB5X5gffekOUhFlt0,45278
|
|
56
|
+
shinestacker/gui/ico/shinestacker.ico,sha256=8IMRk-toObWUz8iDXA-zHBWQ8Ps3vXN5u5ZEyw7sP3c,109613
|
|
57
|
+
shinestacker/gui/ico/shinestacker.png,sha256=VybGY-nig_-wMW8g_uImGxRYvcnltWcAVEPSX6AZUHM,22448
|
|
58
|
+
shinestacker/gui/ico/shinestacker.svg,sha256=r8jx5aiIT9K70MRP0ANWniFE0ctRCqH7LMQ7vcGJ8ss,6571
|
|
59
|
+
shinestacker/gui/img/close-round-line-icon.png,sha256=9HZwCjgni1s_JGUPUb_MoOfoe4tRZgM5OWzk92XFZlE,8019
|
|
60
|
+
shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzdsHvmDAjlbE18Pgo,4788
|
|
61
|
+
shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
|
|
62
|
+
shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
|
|
63
|
+
shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
+
shinestacker/retouch/base_filter.py,sha256=7UfuhZV3wEZefQbExVIHJ6IqKnXlMF2MelxswTMaqOo,10356
|
|
65
|
+
shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
|
|
66
|
+
shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
|
|
67
|
+
shinestacker/retouch/brush_preview.py,sha256=QKD3pL7n7YJbIibinUFYKv7lkyq_AWLpt6oyqnltrwQ,4893
|
|
68
|
+
shinestacker/retouch/brush_tool.py,sha256=nxnEuvTioPNw1WeWsT20X1zl-LNZ8i-1ExOcihikEjk,8618
|
|
69
|
+
shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
|
|
70
|
+
shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
|
|
71
|
+
shinestacker/retouch/exif_data.py,sha256=giqoIaMlhN6H3x8BAd73ghVHODmWIcD_QbSoDymQycU,1864
|
|
72
|
+
shinestacker/retouch/file_loader.py,sha256=x8lzKShlgElcJYLFiDoeszeVEToUUiUbUrozAeF5SMU,4812
|
|
73
|
+
shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
|
|
74
|
+
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
75
|
+
shinestacker/retouch/image_editor_ui.py,sha256=GNozK_P7orgl8EioemJeyR4e_LLMbKVZi4xhYW9m38U,29893
|
|
76
|
+
shinestacker/retouch/image_viewer.py,sha256=3ebrLHTDtGd_EbIT2nNFRUjH836rblmmK7jZ62YcJ2U,19564
|
|
77
|
+
shinestacker/retouch/io_gui_handler.py,sha256=M9rlfqiIaP1wWCm6p-Ew15w0_pH8-DoBXJdEkTUeVCU,11525
|
|
78
|
+
shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
|
|
79
|
+
shinestacker/retouch/layer_collection.py,sha256=Q7zoCYRn__jYkfrEC2lY1uKHWfOUbsJ27xaYHIoKVxo,5730
|
|
80
|
+
shinestacker/retouch/shortcuts_help.py,sha256=SN4vNa_6yRAFaWxt5HpWn8FHgwmHrIs_wYwjl4iyDmg,3769
|
|
81
|
+
shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
|
|
82
|
+
shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
|
|
83
|
+
shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
|
|
84
|
+
shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
|
|
85
|
+
shinestacker-1.0.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
86
|
+
shinestacker-1.0.0.dist-info/METADATA,sha256=600w-Vjn2cDAvudN_Qt4nN3qkxJLmzgNo0a5j6-Hz3E,5957
|
|
87
|
+
shinestacker-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
88
|
+
shinestacker-1.0.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
89
|
+
shinestacker-1.0.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
90
|
+
shinestacker-1.0.0.dist-info/RECORD,,
|
shinestacker/app/app_config.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, C0103, W0201
|
|
2
|
-
from .. config.config import _ConfigBase
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class _AppConfig(_ConfigBase):
|
|
6
|
-
def __new__(cls):
|
|
7
|
-
return _ConfigBase.__new__(cls)
|
|
8
|
-
|
|
9
|
-
def _init_defaults(self):
|
|
10
|
-
self._DONT_USE_NATIVE_MENU = True
|
|
11
|
-
self._COMBINED_APP = False
|
|
12
|
-
|
|
13
|
-
@property
|
|
14
|
-
def DONT_USE_NATIVE_MENU(self):
|
|
15
|
-
return self._DONT_USE_NATIVE_MENU
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def COMBINED_APP(self):
|
|
19
|
-
return self._COMBINED_APP
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
app_config = _AppConfig()
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
|
|
2
|
-
import os.path
|
|
3
|
-
import os
|
|
4
|
-
# import traceback
|
|
5
|
-
import json
|
|
6
|
-
import jsonpickle
|
|
7
|
-
from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
|
|
8
|
-
from .. core.core_utils import get_app_base_path
|
|
9
|
-
from .. config.constants import constants
|
|
10
|
-
from .project_model import ActionConfig
|
|
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.update_title()
|
|
20
|
-
|
|
21
|
-
def update_title(self):
|
|
22
|
-
title = constants.APP_TITLE
|
|
23
|
-
file_name = self.current_file_name()
|
|
24
|
-
if file_name:
|
|
25
|
-
title += f" - {file_name}"
|
|
26
|
-
if self._modified_project:
|
|
27
|
-
title += " *"
|
|
28
|
-
self.window().setWindowTitle(title)
|
|
29
|
-
|
|
30
|
-
def mark_as_modified(self):
|
|
31
|
-
self._modified_project = True
|
|
32
|
-
self.project_buffer.append(self.project.clone())
|
|
33
|
-
self.save_actions_set_enabled(True)
|
|
34
|
-
self.update_title()
|
|
35
|
-
|
|
36
|
-
def close_project(self):
|
|
37
|
-
if self._check_unsaved_changes():
|
|
38
|
-
self.set_project(Project())
|
|
39
|
-
self.set_current_file_path('')
|
|
40
|
-
self.update_title()
|
|
41
|
-
self.job_list.clear()
|
|
42
|
-
self.action_list.clear()
|
|
43
|
-
self._modified_project = False
|
|
44
|
-
self.save_actions_set_enabled(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.set_current_file_path('')
|
|
51
|
-
self._modified_project = False
|
|
52
|
-
self.update_title()
|
|
53
|
-
self.job_list.clear()
|
|
54
|
-
self.action_list.clear()
|
|
55
|
-
self.set_project(Project())
|
|
56
|
-
self.save_actions_set_enabled(False)
|
|
57
|
-
dialog = NewProjectDialog(self)
|
|
58
|
-
if dialog.exec() == QDialog.Accepted:
|
|
59
|
-
self.save_actions_set_enabled(True)
|
|
60
|
-
input_folder = dialog.get_input_folder().split('/')
|
|
61
|
-
working_path = '/'.join(input_folder[:-1])
|
|
62
|
-
input_path = input_folder[-1]
|
|
63
|
-
if dialog.get_noise_detection():
|
|
64
|
-
job_noise = ActionConfig(constants.ACTION_JOB,
|
|
65
|
-
{'name': 'detect-noise', 'working_path': working_path,
|
|
66
|
-
'input_path': input_path})
|
|
67
|
-
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
68
|
-
{'name': 'detect-noise'})
|
|
69
|
-
job_noise.add_sub_action(noise_detection)
|
|
70
|
-
self.project.jobs.append(job_noise)
|
|
71
|
-
job = ActionConfig(constants.ACTION_JOB,
|
|
72
|
-
{'name': 'focus-stack', 'working_path': working_path,
|
|
73
|
-
'input_path': input_path})
|
|
74
|
-
if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
|
|
75
|
-
dialog.get_align_frames() or dialog.get_balance_frames():
|
|
76
|
-
combo_action = ActionConfig(constants.ACTION_COMBO, {'name': 'align'})
|
|
77
|
-
if dialog.get_noise_detection():
|
|
78
|
-
mask_noise = ActionConfig(constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
|
|
79
|
-
combo_action.add_sub_action(mask_noise)
|
|
80
|
-
if dialog.get_vignetting_correction():
|
|
81
|
-
vignetting = ActionConfig(constants.ACTION_VIGNETTING, {'name': 'vignetting'})
|
|
82
|
-
combo_action.add_sub_action(vignetting)
|
|
83
|
-
if dialog.get_align_frames():
|
|
84
|
-
align = ActionConfig(constants.ACTION_ALIGNFRAMES, {'name': 'align'})
|
|
85
|
-
combo_action.add_sub_action(align)
|
|
86
|
-
if dialog.get_balance_frames():
|
|
87
|
-
balance = ActionConfig(constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
|
|
88
|
-
combo_action.add_sub_action(balance)
|
|
89
|
-
job.add_sub_action(combo_action)
|
|
90
|
-
if dialog.get_bunch_stack():
|
|
91
|
-
bunch_stack = ActionConfig(constants.ACTION_FOCUSSTACKBUNCH,
|
|
92
|
-
{'name': 'bunches', 'frames': dialog.get_bunch_frames(),
|
|
93
|
-
'overlap': dialog.get_bunch_overlap()})
|
|
94
|
-
job.add_sub_action(bunch_stack)
|
|
95
|
-
if dialog.get_focus_stack_pyramid():
|
|
96
|
-
focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
97
|
-
{'name': 'focus-stack-pyramid',
|
|
98
|
-
'stacker': constants.STACK_ALGO_PYRAMID})
|
|
99
|
-
job.add_sub_action(focus_pyramid)
|
|
100
|
-
if dialog.get_focus_stack_depth_map():
|
|
101
|
-
focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
102
|
-
{'name': 'focus-stack-depth-map',
|
|
103
|
-
'stacker': constants.STACK_ALGO_DEPTH_MAP})
|
|
104
|
-
job.add_sub_action(focus_depth_map)
|
|
105
|
-
if dialog.get_multi_layer():
|
|
106
|
-
input_path = []
|
|
107
|
-
if dialog.get_focus_stack_pyramid():
|
|
108
|
-
input_path.append("focus-stack-pyramid")
|
|
109
|
-
if dialog.get_focus_stack_depth_map():
|
|
110
|
-
input_path.append("focus-stack-depth-map")
|
|
111
|
-
if dialog.get_bunch_stack():
|
|
112
|
-
input_path.append("bunches")
|
|
113
|
-
else:
|
|
114
|
-
input_path.append(input_path)
|
|
115
|
-
multi_layer = ActionConfig(constants.ACTION_MULTILAYER,
|
|
116
|
-
{'name': 'multi-layer',
|
|
117
|
-
'input_path': ','.join(input_path)})
|
|
118
|
-
job.add_sub_action(multi_layer)
|
|
119
|
-
self.project.jobs.append(job)
|
|
120
|
-
self._modified_project = True
|
|
121
|
-
self.refresh_ui(0, -1)
|
|
122
|
-
|
|
123
|
-
def open_project(self, file_path=False):
|
|
124
|
-
if not self._check_unsaved_changes():
|
|
125
|
-
return
|
|
126
|
-
if file_path is False:
|
|
127
|
-
file_path, _ = QFileDialog.getOpenFileName(
|
|
128
|
-
self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
|
|
129
|
-
if file_path:
|
|
130
|
-
try:
|
|
131
|
-
self.set_current_file_path(file_path)
|
|
132
|
-
with open(self.current_file_path(), 'r', encoding="utf-8") as file:
|
|
133
|
-
json_obj = json.load(file)
|
|
134
|
-
project = Project.from_dict(json_obj['project'])
|
|
135
|
-
if project is None:
|
|
136
|
-
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
137
|
-
self.set_project(project)
|
|
138
|
-
self._modified_project = False
|
|
139
|
-
self.update_title()
|
|
140
|
-
self.refresh_ui(0, -1)
|
|
141
|
-
if self.job_list.count() > 0:
|
|
142
|
-
self.job_list.setCurrentRow(0)
|
|
143
|
-
except Exception as e:
|
|
144
|
-
# traceback.print_tb(e.__traceback__)
|
|
145
|
-
QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
|
|
146
|
-
if len(self.project.jobs) > 0:
|
|
147
|
-
self.job_list.setCurrentRow(0)
|
|
148
|
-
self.activateWindow()
|
|
149
|
-
self.save_actions_set_enabled(True)
|
|
150
|
-
for job in self.project.jobs:
|
|
151
|
-
if 'working_path' in job.params.keys():
|
|
152
|
-
working_path = job.params['working_path']
|
|
153
|
-
if not os.path.isdir(working_path):
|
|
154
|
-
QMessageBox.warning(
|
|
155
|
-
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(
|
|
166
|
-
self, "Working path not found",
|
|
167
|
-
f'''The working path specified in the project file for the job:
|
|
168
|
-
"{job.params['name']}"
|
|
169
|
-
was not found.\n
|
|
170
|
-
Please, select a valid working path.''')
|
|
171
|
-
self.edit_action(action)
|
|
172
|
-
|
|
173
|
-
def save_project(self):
|
|
174
|
-
path = self.current_file_path()
|
|
175
|
-
if path:
|
|
176
|
-
self.do_save(path)
|
|
177
|
-
else:
|
|
178
|
-
self.save_project_as()
|
|
179
|
-
|
|
180
|
-
def save_project_as(self):
|
|
181
|
-
file_path, _ = QFileDialog.getSaveFileName(
|
|
182
|
-
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.do_save(file_path)
|
|
187
|
-
self.set_current_file_path(file_path)
|
|
188
|
-
self._modified_project = False
|
|
189
|
-
self.update_title()
|
|
190
|
-
os.chdir(os.path.dirname(file_path))
|
|
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
|
-
with open(file_path, 'w', encoding="utf-8") as f:
|
|
199
|
-
f.write(json_obj)
|
|
200
|
-
self._modified_project = False
|
|
201
|
-
except Exception as e:
|
|
202
|
-
QMessageBox.critical(self, "Error", f"Cannot save file:\n{str(e)}")
|
|
203
|
-
|
|
204
|
-
def _check_unsaved_changes(self) -> bool:
|
|
205
|
-
if self._modified_project:
|
|
206
|
-
reply = QMessageBox.question(
|
|
207
|
-
self, "Unsaved Changes",
|
|
208
|
-
"The project has unsaved changes. Do you want to continue?",
|
|
209
|
-
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
|
|
210
|
-
)
|
|
211
|
-
if reply == QMessageBox.Save:
|
|
212
|
-
self.save_project()
|
|
213
|
-
return True
|
|
214
|
-
return reply == QMessageBox.Discard
|
|
215
|
-
return True
|
|
216
|
-
|
|
217
|
-
def on_job_edit(self, item):
|
|
218
|
-
index = self.job_list.row(item)
|
|
219
|
-
if 0 <= index < len(self.project.jobs):
|
|
220
|
-
job = self.project.jobs[index]
|
|
221
|
-
dialog = self.action_config_dialog(job)
|
|
222
|
-
if dialog.exec() == QDialog.Accepted:
|
|
223
|
-
current_row = self.job_list.currentRow()
|
|
224
|
-
if current_row >= 0:
|
|
225
|
-
self.job_list.item(current_row).setText(job.params['name'])
|
|
226
|
-
self.refresh_ui()
|
|
227
|
-
|
|
228
|
-
def on_action_edit(self, item):
|
|
229
|
-
job_index = self.job_list.currentRow()
|
|
230
|
-
if 0 <= job_index < len(self.project.jobs):
|
|
231
|
-
job = self.project.jobs[job_index]
|
|
232
|
-
action_index = self.action_list.row(item)
|
|
233
|
-
current_action, is_sub_action = self.get_current_action_at(job, action_index)
|
|
234
|
-
if current_action:
|
|
235
|
-
if not is_sub_action:
|
|
236
|
-
self.set_enabled_sub_actions_gui(
|
|
237
|
-
current_action.type_name == constants.ACTION_COMBO)
|
|
238
|
-
dialog = self.action_config_dialog(current_action)
|
|
239
|
-
if dialog.exec() == QDialog.Accepted:
|
|
240
|
-
self.on_job_selected(job_index)
|
|
241
|
-
self.refresh_ui()
|
|
242
|
-
self.job_list.setCurrentRow(job_index)
|
|
243
|
-
self.action_list.setCurrentRow(action_index)
|
|
244
|
-
|
|
245
|
-
def edit_current_action(self):
|
|
246
|
-
current_action = None
|
|
247
|
-
job_row = self.job_list.currentRow()
|
|
248
|
-
if 0 <= job_row < len(self.project.jobs):
|
|
249
|
-
job = self.project.jobs[job_row]
|
|
250
|
-
if self.job_list.hasFocus():
|
|
251
|
-
current_action = job
|
|
252
|
-
elif self.action_list.hasFocus():
|
|
253
|
-
job_row, _action_row, pos = self.get_current_action()
|
|
254
|
-
if pos.actions is not None:
|
|
255
|
-
current_action = pos.action if not pos.is_sub_action else pos.sub_action
|
|
256
|
-
if current_action is not None:
|
|
257
|
-
self.edit_action(current_action)
|
|
258
|
-
|
|
259
|
-
def edit_action(self, action):
|
|
260
|
-
dialog = self.action_config_dialog(action)
|
|
261
|
-
if dialog.exec() == QDialog.Accepted:
|
|
262
|
-
self.on_job_selected(self.job_list.currentRow())
|
|
263
|
-
self.mark_as_modified()
|
|
264
|
-
|
|
265
|
-
def save_actions_set_enabled(self, enabled):
|
|
266
|
-
pass
|