shinestacker 1.9.0__py3-none-any.whl → 1.9.2__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.

@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, R0913, R0917
1
+ # pylint: disable=C0114, C0115, C0116, R0913, R0917, W0718
2
2
  import os
3
3
  import traceback
4
4
  import logging
@@ -82,6 +82,10 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
82
82
 
83
83
 
84
84
  def get_bunches(collection, n_frames, n_overlap):
85
+ if n_frames == n_overlap:
86
+ raise RuntimeError(
87
+ f"Can't get bunch collection, total number of frames ({n_frames}) "
88
+ "is equal to the number of overlapping grames")
85
89
  bunches = [collection[x:x + n_frames]
86
90
  for x in range(0, len(collection) - n_overlap, n_frames - n_overlap)]
87
91
  return bunches
@@ -109,7 +113,7 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
109
113
 
110
114
  def begin(self):
111
115
  SequentialTask.begin(self)
112
- self._chunks = get_bunches(self.input_filepaths(), self.frames, self.overlap)
116
+ self._chunks = get_bunches(sorted(self.input_filepaths()), self.frames, self.overlap)
113
117
  self.set_counts(len(self._chunks))
114
118
 
115
119
  def end(self):
@@ -122,9 +126,9 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
122
126
  self.print_message(
123
127
  color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
124
128
  constants.LOG_COLOR_LEVEL_2))
125
- img_files = self._chunks[action_count - 1]
129
+ img_files = self._chunks[action_count]
126
130
  self.stack_algo.init(img_files)
127
- self.focus_stack(self._chunks[action_count - 1])
131
+ self.focus_stack(self._chunks[action_count])
128
132
  return True
129
133
 
130
134
 
shinestacker/app/main.py CHANGED
@@ -199,7 +199,7 @@ class MainApp(QMainWindow):
199
199
  class Application(QApplication):
200
200
  def event(self, event):
201
201
  if event.type() == QEvent.Quit and event.spontaneous():
202
- if not self.quit():
202
+ if not self.main_app.quit():
203
203
  return True
204
204
  return super().event(event)
205
205
 
@@ -372,10 +372,6 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
372
372
  self.add_field_to_layout(
373
373
  self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
374
374
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
375
- self.add_field_to_layout(
376
- self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
377
- 'Scratch output folder before run',
378
- required=False, default=True)
379
375
  self.add_field_to_layout(
380
376
  self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
381
377
  'Delete output at end of job',
@@ -12,6 +12,7 @@ class ConfigDialog(QDialog):
12
12
  self.form_layout = create_form_layout(self)
13
13
  scroll_area = QScrollArea()
14
14
  scroll_area.setWidgetResizable(True)
15
+ scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
15
16
  container_widget = QWidget()
16
17
  self.container_layout = QFormLayout(container_widget)
17
18
  self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
@@ -19,19 +20,19 @@ class ConfigDialog(QDialog):
19
20
  self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
20
21
  self.container_layout.setLabelAlignment(Qt.AlignLeft)
21
22
  scroll_area.setWidget(container_widget)
22
- button_box = QHBoxLayout()
23
+ self.button_box = QHBoxLayout()
23
24
  self.ok_button = QPushButton("OK")
24
25
  self.ok_button.setFocus()
25
26
  self.cancel_button = QPushButton("Cancel")
26
27
  self.reset_button = QPushButton("Reset")
27
- button_box.addWidget(self.ok_button)
28
- button_box.addWidget(self.cancel_button)
29
- button_box.addWidget(self.reset_button)
28
+ self.button_box.addWidget(self.ok_button)
29
+ self.button_box.addWidget(self.cancel_button)
30
+ self.button_box.addWidget(self.reset_button)
30
31
  self.reset_button.clicked.connect(self.reset_to_defaults)
31
32
  self.ok_button.clicked.connect(self.accept)
32
33
  self.cancel_button.clicked.connect(self.reject)
33
34
  self.form_layout.addRow(scroll_area)
34
- self.form_layout.addRow(button_box)
35
+ self.form_layout.addRow(self.button_box)
35
36
  QTimer.singleShot(0, self.adjust_dialog_size)
36
37
  self.create_form_content()
37
38
 
@@ -318,6 +318,12 @@ class MainWindow(QMainWindow, LogManager):
318
318
  edit_config_action.triggered.connect(self.edit_current_action)
319
319
  menu.addAction(edit_config_action)
320
320
  menu.addSeparator()
321
+ menu.addAction(self.menu_manager.cut_action)
322
+ menu.addAction(self.menu_manager.copy_action)
323
+ menu.addAction(self.menu_manager.paste_action)
324
+ menu.addAction(self.menu_manager.duplicate_action)
325
+ menu.addAction(self.menu_manager.delete_element_action)
326
+ menu.addSeparator()
321
327
  menu.addAction(self.menu_manager.run_job_action)
322
328
  menu.addAction(self.menu_manager.run_all_jobs_action)
323
329
  menu.addSeparator()
@@ -123,8 +123,14 @@ class MenuManager(QObject):
123
123
  self.undo_action = self.action("&Undo")
124
124
  self.undo_action.setEnabled(False)
125
125
  menu.addAction(self.undo_action)
126
- for name in ["&Cut", "Cop&y", "&Paste", "Duplicate"]:
127
- menu.addAction(self.action(name, requires_file=True))
126
+ self.cut_action = self.action("&Cut", requires_file=True)
127
+ menu.addAction(self.cut_action)
128
+ self.copy_action = self.action("Cop&y", requires_file=True)
129
+ menu.addAction(self.copy_action)
130
+ self.paste_action = self.action("&Paste", requires_file=True)
131
+ menu.addAction(self.paste_action)
132
+ self.duplicate_action = self.action("Duplicate", requires_file=True)
133
+ menu.addAction(self.duplicate_action)
128
134
  self.delete_element_action = self.action("Delete", requires_file=True)
129
135
  self.delete_element_action.setEnabled(False)
130
136
  menu.addAction(self.delete_element_action)
@@ -75,6 +75,26 @@ class BrushTool:
75
75
  self.hardness_slider.setValue(val)
76
76
  self.update_brush_hardness(val)
77
77
 
78
+ def increase_brush_opacity(self, amount=2):
79
+ val = min(self.opacity_slider.value() + amount, self.opacity_slider.maximum())
80
+ self.opacity_slider.setValue(val)
81
+ self.update_brush_opacity(val)
82
+
83
+ def decrease_brush_opacity(self, amount=2):
84
+ val = max(self.opacity_slider.value() - amount, self.opacity_slider.minimum())
85
+ self.opacity_slider.setValue(val)
86
+ self.update_brush_opacity(val)
87
+
88
+ def increase_brush_flow(self, amount=2):
89
+ val = min(self.flow_slider.value() + amount, self.flow_slider.maximum())
90
+ self.flow_slider.setValue(val)
91
+ self.update_brush_flow(val)
92
+
93
+ def decrease_brush_flow(self, amount=2):
94
+ val = max(self.flow_slider.value() - amount, self.flow_slider.minimum())
95
+ self.flow_slider.setValue(val)
96
+ self.update_brush_flow(val)
97
+
78
98
  def update_brush_hardness(self, hardness):
79
99
  self.brush.hardness = hardness
80
100
  self.update_brush_thumb()
@@ -1,55 +1,75 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718
2
+ from xml.dom import minidom
2
3
  from PIL.TiffImagePlugin import IFDRational
3
- from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QLabel
4
+ from PySide6.QtWidgets import QLabel, QTextEdit
4
5
  from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QFontDatabase
5
7
  from .. algorithms.exif import exif_dict
6
- from .icon_container import icon_container
7
- from .. gui.base_form_dialog import BaseFormDialog
8
+ from .. gui.config_dialog import ConfigDialog
8
9
 
9
10
 
10
- class ExifData(BaseFormDialog):
11
- def __init__(self, exif, parent=None):
12
- super().__init__("EXIF data", parent=parent)
11
+ class ExifData(ConfigDialog):
12
+ def __init__(self, exif, title="EXIF Data", parent=None, show_buttons=True):
13
13
  self.exif = exif
14
- self.create_form()
15
- button_container = QWidget()
16
- button_layout = QHBoxLayout(button_container)
17
- button_layout.setAlignment(Qt.AlignCenter)
18
- ok_button = QPushButton("OK")
19
- ok_button.setFixedWidth(100)
20
- ok_button.setFocus()
21
- button_layout.addWidget(ok_button)
22
- self.add_row_to_layout(button_container)
23
- ok_button.clicked.connect(self.accept)
14
+ super().__init__(title, parent)
15
+ self.reset_button.setVisible(False)
16
+ self.cancel_button.setVisible(show_buttons)
17
+ if not show_buttons:
18
+ self.ok_button.setFixedWidth(100)
19
+ self.button_box.setAlignment(Qt.AlignCenter)
24
20
 
25
- def add_bold_label(self, label):
26
- label = QLabel(label)
27
- label.setStyleSheet("font-weight: bold")
28
- self.form_layout.addRow(label)
21
+ def is_likely_xml(self, text):
22
+ if not isinstance(text, str):
23
+ return False
24
+ text = text.strip()
25
+ return (text.startswith('<?xml') or
26
+ text.startswith('<x:xmpmeta') or
27
+ text.startswith('<rdf:RDF') or
28
+ text.startswith('<?xpacket') or
29
+ (text.startswith('<') and text.endswith('>') and
30
+ any(tag in text for tag in ['<rdf:', '<xmp:', '<dc:', '<tiff:'])))
29
31
 
30
- def create_form(self):
31
- self.form_layout.addRow(icon_container())
32
+ def prettify_xml(self, xml_string):
33
+ try:
34
+ parsed = minidom.parseString(xml_string)
35
+ pretty_xml = parsed.toprettyxml(indent=" ")
36
+ lines = [line for line in pretty_xml.split('\n') if line.strip()]
37
+ if lines and lines[0].startswith('<?xml version="1.0" ?>'):
38
+ lines = lines[1:]
39
+ return '\n'.join(lines)
40
+ except Exception:
41
+ return xml_string
32
42
 
33
- spacer = QLabel("")
34
- spacer.setFixedHeight(10)
35
- self.form_layout.addRow(spacer)
36
- self.add_bold_label("EXIF data")
37
- shortcuts = {}
43
+ def create_form_content(self):
38
44
  if self.exif is None:
39
- shortcuts['Warning:'] = 'no EXIF data found'
40
45
  data = {}
41
46
  else:
42
47
  data = exif_dict(self.exif)
43
48
  if len(data) > 0:
44
49
  for k, (_, d) in data.items():
45
- print(k, type(d))
46
50
  if isinstance(d, IFDRational):
47
51
  d = f"{d.numerator}/{d.denominator}"
48
- elif len(str(d)) > 40:
49
- d = f"{str(d):.40}..."
50
- else:
51
- d = f"{d}"
52
- if "<<<" not in d and k != 'IPTCNAA':
53
- self.form_layout.addRow(f"<b>{k}:</b>", QLabel(d))
52
+ d_str = str(d)
53
+ if "<<<" not in d_str and k != 'IPTCNAA':
54
+ if len(d_str) <= 40:
55
+ self.container_layout.addRow(f"<b>{k}:</b>", QLabel(d_str))
56
+ else:
57
+ if self.is_likely_xml(d_str):
58
+ d_str = self.prettify_xml(d_str)
59
+ text_edit = QTextEdit()
60
+ text_edit.setPlainText(d_str)
61
+ text_edit.setReadOnly(True)
62
+ text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
63
+ text_edit.setLineWrapMode(QTextEdit.WidgetWidth)
64
+ text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
65
+ text_edit.setFixedWidth(400)
66
+ font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
67
+ font.setPointSize(10)
68
+ text_edit.setFont(font)
69
+ font.setPointSize(11)
70
+ text_edit.setFont(font)
71
+ text_edit.setFixedHeight(200)
72
+ text_edit.setFixedHeight(100)
73
+ self.container_layout.addRow(f"<b>{k}:</b>", text_edit)
54
74
  else:
55
- self.form_layout.addRow("-", QLabel("Empty EXIF dictionary"))
75
+ self.container_layout.addRow("No EXIF Data", QLabel(''))
@@ -1,7 +1,8 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904, W0108
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904, W0108, R0911
2
2
  from functools import partial
3
3
  from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QMenu,
4
- QListWidget, QSlider, QMainWindow, QMessageBox)
4
+ QFileDialog, QListWidget, QSlider, QMainWindow, QMessageBox,
5
+ QDialog)
5
6
  from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
6
7
  from PySide6.QtCore import Qt
7
8
  from PySide6.QtGui import QGuiApplication
@@ -9,6 +10,7 @@ from .. config.constants import constants
9
10
  from .. config.app_config import AppConfig
10
11
  from .. config.gui_constants import gui_constants
11
12
  from .. gui.recent_file_manager import RecentFileManager
13
+ from .. algorithms.exif import get_exif
12
14
  from .image_viewer import ImageViewer
13
15
  from .shortcuts_help import ShortcutsHelp
14
16
  from .brush import Brush
@@ -26,6 +28,7 @@ from .white_balance_filter import WhiteBalanceFilter
26
28
  from .vignetting_filter import VignettingFilter
27
29
  from .adjustments import LumiContrastFilter, SaturationVibranceFilter
28
30
  from .transformation_manager import TransfromationManager
31
+ from .exif_data import ExifData
29
32
 
30
33
 
31
34
  class ImageEditorUI(QMainWindow, LayerCollectionHandler):
@@ -57,6 +60,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
57
60
  self.handle_temp_view,
58
61
  self.end_copy_brush_area,
59
62
  self.handle_brush_size_change,
63
+ self.handle_brush_hardness_change,
64
+ self.handle_brush_opacity_change,
65
+ self.handle_brush_flow_change,
60
66
  self.handle_needs_update)
61
67
  side_panel = QWidget()
62
68
  side_layout = QVBoxLayout(side_panel)
@@ -183,6 +189,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
183
189
  self.thumbnail_list.setFixedWidth(gui_constants.THUMB_WIDTH)
184
190
  self.thumbnail_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
185
191
  self.thumbnail_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
192
+ self.exif_dialog = None
186
193
 
187
194
  def change_layer_item(item):
188
195
  layer_idx = self.thumbnail_list.row(item)
@@ -266,8 +273,17 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
266
273
 
267
274
  file_menu.addAction("&Close", self.close_file, "Ctrl+W")
268
275
  file_menu.addSeparator()
276
+ show_exif_action = QAction("Show EXIF Data", self)
277
+ show_exif_action.triggered.connect(self.show_exif_data)
278
+ show_exif_action.setProperty("requires_file", True)
279
+ file_menu.addAction(show_exif_action)
280
+ delete_exif_action = QAction("Delete EXIF Data", self)
281
+ delete_exif_action.triggered.connect(self.delete_exif_data)
282
+ delete_exif_action.setProperty("requires_file", True)
283
+ file_menu.addAction(delete_exif_action)
284
+ file_menu.addSeparator()
269
285
  file_menu.addAction("&Import Frames", self.io_gui_handler.import_frames)
270
- file_menu.addAction("Import &EXIF Data", self.io_gui_handler.select_exif_path)
286
+ file_menu.addAction("Import &EXIF Data", self.select_exif_path)
271
287
 
272
288
  edit_menu = menubar.addMenu("&Edit")
273
289
  self.undo_action = QAction("Undo", self)
@@ -595,6 +611,18 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
595
611
  if event.text() == '}':
596
612
  self.brush_tool.increase_brush_hardness()
597
613
  return
614
+ if event.text() == ',':
615
+ self.brush_tool.decrease_brush_opacity()
616
+ return
617
+ if event.text() == '.':
618
+ self.brush_tool.increase_brush_opacity()
619
+ return
620
+ if event.text() == ';':
621
+ self.brush_tool.decrease_brush_flow()
622
+ return
623
+ if event.text() == ':':
624
+ self.brush_tool.increase_brush_flow()
625
+ return
598
626
  super().keyPressEvent(event)
599
627
  # pylint: enable=C0103
600
628
 
@@ -676,6 +704,36 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
676
704
  self.redo_action.setText("Redo")
677
705
  self.redo_action.setEnabled(False)
678
706
 
707
+ def select_exif_path(self):
708
+ path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
709
+ if path:
710
+ temp_exif_data = get_exif(path)
711
+ self.exif_dialog = ExifData(temp_exif_data, "Import Selected EXIF Data",
712
+ self.parent(), show_buttons=True)
713
+ result = self.exif_dialog.exec()
714
+ if result == QDialog.Accepted:
715
+ self.io_gui_handler.set_exif_data(temp_exif_data, path)
716
+ self.show_status_message(f"EXIF data loaded from {path}.")
717
+ else:
718
+ self.show_status_message("EXIF data loading cancelled.")
719
+
720
+ def show_exif_data(self):
721
+ self.exif_dialog = ExifData(self.io_gui_handler.exif_data, "EXIF Data",
722
+ self.parent(), show_buttons=False)
723
+ self.exif_dialog.exec()
724
+
725
+ def delete_exif_data(self):
726
+ reply = QMessageBox.question(
727
+ self,
728
+ "Confirm Delete",
729
+ "Warning: the current EXIF data will be erased.\n\nDo you want to continue?",
730
+ QMessageBox.Yes | QMessageBox.No,
731
+ QMessageBox.No
732
+ )
733
+ if reply == QMessageBox.Yes:
734
+ self.io_gui_handler.exif_data = None
735
+ self.io_gui_handler.exif_path = ''
736
+
679
737
  def luminosity_filter(self):
680
738
  self.filter_manager.apply("Luminosity, Contrast")
681
739
 
@@ -763,5 +821,23 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
763
821
  else:
764
822
  self.brush_tool.decrease_brush_size()
765
823
 
824
+ def handle_brush_hardness_change(self, delta):
825
+ if delta > 0:
826
+ self.brush_tool.increase_brush_hardness()
827
+ else:
828
+ self.brush_tool.decrease_brush_hardness()
829
+
830
+ def handle_brush_opacity_change(self, delta):
831
+ if delta > 0:
832
+ self.brush_tool.increase_brush_opacity()
833
+ else:
834
+ self.brush_tool.decrease_brush_opacity()
835
+
836
+ def handle_brush_flow_change(self, delta):
837
+ if delta > 0:
838
+ self.brush_tool.increase_brush_flow()
839
+ else:
840
+ self.brush_tool.decrease_brush_flow()
841
+
766
842
  def handle_set_zoom_factor(self, zoom_factor):
767
843
  self.zoom_factor_label.setText(f"zoom: {zoom_factor:.1%}")
@@ -132,10 +132,15 @@ class ImageViewer(QWidget):
132
132
 
133
133
  def connect_signals(
134
134
  self, handle_temp_view, end_copy_brush_area,
135
- handle_brush_size_change, handle_needs_update):
135
+ handle_brush_size_change, handle_brush_hardness_change,
136
+ handle_brush_opacity_change, handle_brush_flow_change,
137
+ handle_needs_update):
136
138
  for st in self._strategies.values():
137
139
  st.temp_view_requested.connect(handle_temp_view)
138
140
  st.end_copy_brush_area_requested.connect(end_copy_brush_area)
139
141
  st.brush_size_change_requested.connect(handle_brush_size_change)
142
+ st.brush_hardness_change_requested.connect(handle_brush_hardness_change)
143
+ st.brush_opacity_change_requested.connect(handle_brush_opacity_change)
144
+ st.brush_flow_change_requested.connect(handle_brush_flow_change)
140
145
  st.needs_update_requested.connect(handle_needs_update)
141
146
  st.setFocusPolicy(Qt.StrongFocus)
@@ -10,7 +10,6 @@ from PySide6.QtCore import Qt, QObject, QTimer, Signal
10
10
  from .. algorithms.utils import EXTENSIONS_GUI_STR, EXTENSIONS_GUI_SAVE_STR
11
11
  from .. algorithms.exif import get_exif, write_image_with_exif_data
12
12
  from .file_loader import FileLoader
13
- from .exif_data import ExifData
14
13
  from .io_threads import FileMultilayerSaver, FrameImporter
15
14
  from .layer_collection import LayerCollectionHandler
16
15
 
@@ -33,7 +32,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
33
32
  self.image_viewer = None
34
33
  self.loading_dialog = None
35
34
  self.loading_timer = None
36
- self.exif_dialog = None
37
35
  self.saver_thread = None
38
36
  self.saving_dialog = None
39
37
  self.saving_timer = None
@@ -47,6 +45,10 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
47
45
  self.exif_data = None
48
46
  self.exif_path = ''
49
47
 
48
+ def set_exif_data(self, data, path):
49
+ self.exif_data = data
50
+ self.exif_path = path
51
+
50
52
  def current_file_path(self):
51
53
  return self.current_file_path_master if self.save_master_only.isChecked() \
52
54
  else self.current_file_path_multi
@@ -164,6 +166,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
164
166
  self.loader_thread.finished.connect(self.on_file_loaded)
165
167
  self.loader_thread.error.connect(self.on_file_error)
166
168
  self.loader_thread.start()
169
+ self.exif_path = self.current_file_path_master
170
+ self.exif_data = get_exif(self.exif_path)
167
171
 
168
172
  def import_frames(self):
169
173
  file_paths, _ = QFileDialog.getOpenFileNames(
@@ -196,6 +200,9 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
196
200
  self.frame_importer_thread.error.connect(self.on_frames_import_error)
197
201
  self.frame_importer_thread.progress.connect(self.update_import_progress)
198
202
  self.frame_importer_thread.start()
203
+ if self.exif_data is None:
204
+ self.exif_path = file_paths[0]
205
+ self.exif_data = get_exif(self.exif_path)
199
206
 
200
207
  def update_import_progress(self, percent, filename):
201
208
  if hasattr(self, 'progress_bar'):
@@ -296,7 +303,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
296
303
  img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
297
304
  write_image_with_exif_data(self.exif_data, img, path)
298
305
  self.current_file_path_master = os.path.abspath(path)
299
- # self.mark_as_modified_requested.emit(False)
300
306
  self.update_title_requested.emit()
301
307
  self.add_recent_file_requested.emit(self.current_file_path_master)
302
308
  self.status_message_requested.emit(f"Saved master layer to: {path}")
@@ -304,15 +310,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
304
310
  traceback.print_tb(e.__traceback__)
305
311
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
306
312
 
307
- def select_exif_path(self):
308
- path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
309
- if path:
310
- self.exif_path = path
311
- self.exif_data = get_exif(path)
312
- self.status_message_requested.emit(f"EXIF data extracted from {path}.")
313
- self.exif_dialog = ExifData(self.exif_data, self.parent())
314
- self.exif_dialog.exec()
315
-
316
313
  def close_file(self):
317
314
  self.mark_as_modified_requested.emit(False)
318
315
  self.layer_collection.reset()
@@ -61,7 +61,10 @@ class ShortcutsHelp(QDialog):
61
61
  "Ctrl + +": "Zoom in",
62
62
  "Ctrl + -": "Zoom out",
63
63
  "Ctrl + 0": "Fit to screen",
64
- "Ctrl + R": "Actual size"
64
+ "Ctrl + R": "Actual size",
65
+ "Ctrl + 1": "View: overlaid",
66
+ "Ctrl + 2": "View: side by side",
67
+ "Ctrl + 3": "View: top-bottom",
65
68
  }
66
69
 
67
70
  self.add_bold_label(left_layout, "Keyboard Shortcuts")
@@ -69,13 +72,14 @@ class ShortcutsHelp(QDialog):
69
72
  left_layout.addRow(f"<b>{k}</b>", QLabel(v))
70
73
 
71
74
  shortcuts = {
72
- "Ctrl + 1": "View: overlaid",
73
- "Ctrl + 2": "View: side by side",
74
- "Ctrl + 3": "View: top-bottom",
75
- "[": "Increase brush size",
76
- "]": "Decrease brush size",
77
- "{": "Increase brush hardness",
78
- "}": "Decrease brush hardness"
75
+ "[": "Decrease brush size",
76
+ "]": "Increase brush size",
77
+ "{": "Decrease brush hardness",
78
+ "}": "Increase brush hardness",
79
+ ",": "Decrease brush opacity",
80
+ ".": "Increase brush opacity",
81
+ ";": "Decrease brush flow",
82
+ ":": "Increase brush flow"
79
83
  }
80
84
 
81
85
  self.add_bold_label(right_layout, "Keyboard Shortcuts")
@@ -86,6 +90,9 @@ class ShortcutsHelp(QDialog):
86
90
  "Space + Drag": "Move",
87
91
  "Wheel": "Zoom in/out",
88
92
  "Ctrl + Wheel": "Adjust brush size",
93
+ "Shift + Wheel": "Adjust brush hardness",
94
+ "Alt + Wheel": "Adjust brush opacity",
95
+ "Ctrl + Shift + Wheel": "Adjust brush flow",
89
96
  "Left Click": "Use brush to copy from selected layer to master",
90
97
  }
91
98
 
@@ -6,7 +6,7 @@ import numpy as np
6
6
  from PySide6.QtCore import Qt, QPointF, QTime, QPoint, Signal, QRectF
7
7
  from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor, QPixmap, QPainterPath
8
8
  from PySide6.QtWidgets import (
9
- QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
9
+ QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QApplication,
10
10
  QGraphicsItemGroup, QGraphicsPathItem)
11
11
  from .. config.gui_constants import gui_constants
12
12
  from .. config.app_config import AppConfig
@@ -81,6 +81,9 @@ class ViewSignals:
81
81
  temp_view_requested = Signal(bool)
82
82
  end_copy_brush_area_requested = Signal()
83
83
  brush_size_change_requested = Signal(int) # +1 or -1
84
+ brush_hardness_change_requested = Signal(int)
85
+ brush_opacity_change_requested = Signal(int)
86
+ brush_flow_change_requested = Signal(int)
84
87
  needs_update_requested = Signal()
85
88
 
86
89
 
@@ -444,8 +447,15 @@ class ViewStrategy(LayerCollectionHandler):
444
447
  if self.empty() or self.gesture_active:
445
448
  return
446
449
  if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
447
- if self.control_pressed:
450
+ modifiers = QApplication.keyboardModifiers()
451
+ if modifiers & Qt.ControlModifier and modifiers & Qt.ShiftModifier:
452
+ self.brush_flow_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
453
+ elif modifiers & Qt.ControlModifier:
448
454
  self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
455
+ elif modifiers & Qt.ShiftModifier:
456
+ self.brush_hardness_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
457
+ elif modifiers & Qt.AltModifier:
458
+ self.brush_opacity_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
449
459
  else:
450
460
  self.handle_zoom_wheel(self.get_view_with_mouse(event), event)
451
461
  self.update_brush_cursor()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.9.0
3
+ Version: 1.9.2
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0