shinestacker 1.3.1__py3-none-any.whl → 1.5.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.

Files changed (38) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +198 -18
  3. shinestacker/algorithms/align_parallel.py +17 -1
  4. shinestacker/algorithms/balance.py +23 -13
  5. shinestacker/algorithms/noise_detection.py +3 -1
  6. shinestacker/algorithms/utils.py +21 -10
  7. shinestacker/algorithms/vignetting.py +2 -0
  8. shinestacker/app/main.py +1 -1
  9. shinestacker/config/gui_constants.py +7 -2
  10. shinestacker/core/core_utils.py +10 -1
  11. shinestacker/gui/action_config.py +172 -7
  12. shinestacker/gui/action_config_dialog.py +246 -285
  13. shinestacker/gui/gui_run.py +2 -2
  14. shinestacker/gui/main_window.py +14 -5
  15. shinestacker/gui/menu_manager.py +26 -2
  16. shinestacker/gui/project_controller.py +4 -0
  17. shinestacker/gui/recent_file_manager.py +93 -0
  18. shinestacker/retouch/base_filter.py +13 -15
  19. shinestacker/retouch/brush_preview.py +3 -1
  20. shinestacker/retouch/brush_tool.py +11 -11
  21. shinestacker/retouch/display_manager.py +43 -59
  22. shinestacker/retouch/image_editor_ui.py +161 -82
  23. shinestacker/retouch/image_view_status.py +65 -0
  24. shinestacker/retouch/image_viewer.py +95 -431
  25. shinestacker/retouch/io_gui_handler.py +12 -2
  26. shinestacker/retouch/layer_collection.py +3 -0
  27. shinestacker/retouch/overlaid_view.py +215 -0
  28. shinestacker/retouch/shortcuts_help.py +13 -3
  29. shinestacker/retouch/sidebyside_view.py +477 -0
  30. shinestacker/retouch/transformation_manager.py +43 -0
  31. shinestacker/retouch/undo_manager.py +22 -3
  32. shinestacker/retouch/view_strategy.py +557 -0
  33. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/METADATA +7 -7
  34. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/RECORD +38 -32
  35. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/WHEEL +0 -0
  36. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/entry_points.txt +0 -0
  37. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/licenses/LICENSE +0 -0
  38. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,557 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0903, R0902, E1101, R0914
2
+ import math
3
+ from abc import abstractmethod
4
+ import numpy as np
5
+ from PySide6.QtCore import Qt, QPointF, QTime, QPoint, Signal, QRectF
6
+ from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor, QPixmap
7
+ from PySide6.QtWidgets import (
8
+ QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem)
9
+ from .. config.gui_constants import gui_constants
10
+ from .layer_collection import LayerCollectionHandler
11
+ from .brush_gradient import create_default_brush_gradient
12
+ from .brush_preview import BrushPreviewItem
13
+
14
+
15
+ class ViewSignals:
16
+ temp_view_requested = Signal(bool)
17
+ brush_operation_started = Signal(QPoint)
18
+ brush_operation_continued = Signal(QPoint)
19
+ brush_operation_ended = Signal()
20
+ brush_size_change_requested = Signal(int) # +1 or -1
21
+
22
+
23
+ class ImageGraphicsViewBase(QGraphicsView):
24
+ def __init__(self, parent=None):
25
+ super().__init__(parent)
26
+ self.setTransformationAnchor(QGraphicsView.AnchorViewCenter)
27
+ self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
28
+ self.setInteractive(False)
29
+ self.grabGesture(Qt.PinchGesture)
30
+ self.grabGesture(Qt.PanGesture)
31
+ self.setMouseTracking(True)
32
+ self.setDragMode(QGraphicsView.NoDrag)
33
+ self.setRenderHint(QPainter.Antialiasing)
34
+ self.setRenderHint(QPainter.SmoothPixmapTransform)
35
+ self.setCursor(Qt.BlankCursor)
36
+
37
+
38
+ class ViewStrategy(LayerCollectionHandler):
39
+ def __init__(self, layer_collection, status):
40
+ LayerCollectionHandler.__init__(self, layer_collection)
41
+ self.display_manager = None
42
+ self.status = status
43
+ self.brush = None
44
+ self.brush_cursor = None
45
+ self.display_manager = None
46
+ self.brush_preview = BrushPreviewItem(layer_collection)
47
+ self.cursor_style = gui_constants.DEFAULT_CURSOR_STYLE
48
+ self.allow_cursor_preview = True
49
+ self.control_pressed = False
50
+ self.space_pressed = False
51
+ self.gesture_active = False
52
+ self.pinch_center_view = None
53
+ self.pinch_center_scene = None
54
+ self.pinch_start_scale = None
55
+ self.scrolling = False
56
+ self.dragging = False
57
+ self.last_brush_pos = None
58
+ self.last_mouse_pos = None
59
+ self.last_update_time = QTime.currentTime()
60
+
61
+ @abstractmethod
62
+ def create_pixmaps(self):
63
+ pass
64
+
65
+ @abstractmethod
66
+ def set_master_image(self, qimage):
67
+ pass
68
+
69
+ @abstractmethod
70
+ def set_current_image(self, qimage):
71
+ pass
72
+
73
+ @abstractmethod
74
+ def get_master_view(self):
75
+ pass
76
+
77
+ @abstractmethod
78
+ def get_current_view(self):
79
+ pass
80
+
81
+ @abstractmethod
82
+ def get_master_scene(self):
83
+ pass
84
+
85
+ @abstractmethod
86
+ def get_current_scene(self):
87
+ pass
88
+
89
+ @abstractmethod
90
+ def get_views(self):
91
+ pass
92
+
93
+ @abstractmethod
94
+ def get_scenes(self):
95
+ pass
96
+
97
+ @abstractmethod
98
+ def get_pixmaps(self):
99
+ pass
100
+
101
+ @abstractmethod
102
+ def get_master_pixmap(self):
103
+ pass
104
+
105
+ @abstractmethod
106
+ def get_current_pixmap(self):
107
+ pass
108
+
109
+ @abstractmethod
110
+ def show_master(self):
111
+ pass
112
+
113
+ @abstractmethod
114
+ def show_current(self):
115
+ pass
116
+
117
+ @abstractmethod
118
+ def arrange_images(self):
119
+ pass
120
+
121
+ def update_master_display(self):
122
+ if not self.empty():
123
+ master_qimage = self.numpy_to_qimage(self.master_layer())
124
+ if master_qimage:
125
+ pixmap = QPixmap.fromImage(master_qimage)
126
+ self.get_master_pixmap().setPixmap(pixmap)
127
+ self.get_master_scene().setSceneRect(QRectF(pixmap.rect()))
128
+ self.get_master_view().horizontalScrollBar().setValue(self.status.h_scroll)
129
+ self.get_master_view().verticalScrollBar().setValue(self.status.v_scroll)
130
+ self.arrange_images()
131
+
132
+ def update_current_display(self):
133
+ if not self.empty() and self.number_of_layers() > 0:
134
+ current_qimage = self.numpy_to_qimage(self.current_layer())
135
+ if current_qimage:
136
+ pixmap = QPixmap.fromImage(current_qimage)
137
+ self.get_current_pixmap().setPixmap(pixmap)
138
+ self.get_current_scene().setSceneRect(QRectF(pixmap.rect()))
139
+ self.get_current_view().horizontalScrollBar().setValue(self.status.h_scroll)
140
+ self.get_current_view().verticalScrollBar().setValue(self.status.v_scroll)
141
+ self.arrange_images()
142
+
143
+ def update_cursor_pen_width(self):
144
+ pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
145
+ if self.brush_cursor is not None:
146
+ master_pen = self.brush_cursor.pen()
147
+ master_pen.setWidthF(pen_width)
148
+ self.brush_cursor.setPen(master_pen)
149
+ return pen_width
150
+
151
+ def set_allow_cursor_preview(self, state):
152
+ self.allow_cursor_preview = state
153
+
154
+ def zoom_factor(self):
155
+ return self.status.zoom_factor
156
+
157
+ def set_zoom_factor(self, zoom_factor):
158
+ self.status.set_zoom_factor(zoom_factor)
159
+
160
+ def get_current_scale(self):
161
+ return self.get_master_view().transform().m11()
162
+
163
+ def min_scale(self):
164
+ return self.status.min_scale
165
+
166
+ def max_scale(self):
167
+ return self.status.max_scale
168
+
169
+ def set_min_scale(self, scale):
170
+ self.status.set_min_scale(scale)
171
+
172
+ def set_max_scale(self, scale):
173
+ self.status.set_max_scale(scale)
174
+
175
+ def empty(self):
176
+ return self.status.empty()
177
+
178
+ def set_brush(self, brush):
179
+ self.brush = brush
180
+
181
+ def set_preview_brush(self, brush):
182
+ self.brush_preview.brush = brush
183
+
184
+ def set_display_manager(self, dm):
185
+ self.display_manager = dm
186
+
187
+ def set_cursor_style(self, style):
188
+ self.cursor_style = style
189
+ if self.brush_cursor:
190
+ self.update_brush_cursor()
191
+
192
+ def get_cursor_style(self):
193
+ return self.cursor_style
194
+
195
+ def handle_key_press_event(self, _event):
196
+ return True
197
+
198
+ def handle_key_release_event(self, _event):
199
+ return True
200
+
201
+ def clear_image(self):
202
+ for scene in self.get_scenes():
203
+ scene.clear()
204
+ self.create_pixmaps()
205
+ self.status.clear()
206
+ self.setup_brush_cursor()
207
+ self.brush_preview = BrushPreviewItem(self.layer_collection)
208
+ self.get_master_scene().addItem(self.brush_preview)
209
+ self.setCursor(Qt.ArrowCursor)
210
+ if self.brush_cursor:
211
+ self.brush_cursor.hide()
212
+
213
+ def cleanup_brush_preview(self):
214
+ if self.brush_cursor:
215
+ self.brush_cursor.hide()
216
+ self.brush_preview.hide()
217
+
218
+ def set_master_image_np(self, img):
219
+ self.set_master_image(self.numpy_to_qimage(img))
220
+
221
+ def numpy_to_qimage(self, array):
222
+ if array is None:
223
+ return None
224
+ if array.dtype == np.uint16:
225
+ array = np.right_shift(array, 8).astype(np.uint8)
226
+ if array.ndim == 2:
227
+ height, width = array.shape
228
+ return QImage(memoryview(array), width, height, width, QImage.Format_Grayscale8)
229
+ if array.ndim == 3:
230
+ height, width, _ = array.shape
231
+ if not array.flags['C_CONTIGUOUS']:
232
+ array = np.ascontiguousarray(array)
233
+ return QImage(memoryview(array), width, height, 3 * width, QImage.Format_RGB888)
234
+ return QImage()
235
+
236
+ def create_scene(self, view):
237
+ scene = QGraphicsScene()
238
+ view.setScene(scene)
239
+ scene.setBackgroundBrush(QBrush(QColor(120, 120, 120)))
240
+ return scene
241
+
242
+ def create_pixmap(self, scene):
243
+ pixmap_item = QGraphicsPixmapItem()
244
+ scene.addItem(pixmap_item)
245
+ return pixmap_item
246
+
247
+ def refresh_display(self):
248
+ for scene in self.get_scenes():
249
+ scene.update()
250
+ self.update_brush_cursor()
251
+
252
+ def set_max_min_scales(self, img_width, img_height):
253
+ self.set_min_scale(min(gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width,
254
+ gui_constants.MIN_ZOOMED_IMG_HEIGHT / img_height))
255
+ self.set_max_scale(gui_constants.MAX_ZOOMED_IMG_PX_SIZE)
256
+
257
+ def zoom_in(self):
258
+ if self.empty():
259
+ return
260
+ master_view = self.get_master_view()
261
+ old_center = master_view.mapToScene(master_view.viewport().rect().center())
262
+ current_scale = self.get_current_scale()
263
+ new_scale = current_scale * gui_constants.ZOOM_IN_FACTOR
264
+ if new_scale <= self.max_scale():
265
+ for view in self.get_views():
266
+ view.scale(gui_constants.ZOOM_IN_FACTOR, gui_constants.ZOOM_IN_FACTOR)
267
+ self.set_zoom_factor(new_scale)
268
+ master_view.centerOn(old_center)
269
+ self.update_brush_cursor()
270
+ self.update_cursor_pen_width()
271
+
272
+ def apply_zoom(self):
273
+ if self.empty():
274
+ return
275
+ for view in self.get_views():
276
+ current_scale = view.transform().m11()
277
+ scale_factor = self.zoom_factor() / current_scale
278
+ view.scale(scale_factor, scale_factor)
279
+
280
+ def zoom_out(self):
281
+ if self.empty():
282
+ return
283
+ master_view = self.get_master_view()
284
+ old_center = master_view.mapToScene(master_view.viewport().rect().center())
285
+ current_scale = self.get_current_scale()
286
+ new_scale = current_scale * gui_constants.ZOOM_OUT_FACTOR
287
+ if new_scale >= self.min_scale():
288
+ for view in self.get_views():
289
+ view.scale(gui_constants.ZOOM_OUT_FACTOR, gui_constants.ZOOM_OUT_FACTOR)
290
+ self.set_zoom_factor(new_scale)
291
+ master_view.centerOn(old_center)
292
+ self.update_brush_cursor()
293
+ self.update_cursor_pen_width()
294
+
295
+ def reset_zoom(self):
296
+ if self.empty():
297
+ return
298
+ self.pinch_start_scale = 1.0
299
+ self.gesture_active = False
300
+ self.pinch_center_view = None
301
+ self.pinch_center_scene = None
302
+ for pixmap, view in self.get_pixmaps().items():
303
+ view.fitInView(pixmap, Qt.KeepAspectRatio)
304
+ self.set_zoom_factor(self.get_current_scale())
305
+ self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), self.zoom_factor())))
306
+ for view in self.get_views():
307
+ view.resetTransform()
308
+ view.scale(self.zoom_factor(), self.zoom_factor())
309
+ self.update_brush_cursor()
310
+ self.update_cursor_pen_width()
311
+
312
+ def actual_size(self):
313
+ if self.empty():
314
+ return
315
+ self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), 1.0)))
316
+ for view in self.get_views():
317
+ view.resetTransform()
318
+ view.scale(self.zoom_factor(), self.zoom_factor())
319
+ self.update_brush_cursor()
320
+ self.update_cursor_pen_width()
321
+
322
+ def setup_outline_style(self):
323
+ self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
324
+ gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
325
+ self.brush_cursor.setBrush(Qt.NoBrush)
326
+
327
+ def setup_simple_brush_style(self, center_x, center_y, radius):
328
+ gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
329
+ self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
330
+ gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
331
+ self.brush_cursor.setBrush(QBrush(gradient))
332
+
333
+ def setup_brush_cursor(self):
334
+ if not self.brush:
335
+ return
336
+ scene = self.get_master_scene()
337
+ for item in scene.items():
338
+ if isinstance(item, QGraphicsEllipseItem) and item != self.brush_preview:
339
+ scene.removeItem(item)
340
+ pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
341
+ brush = Qt.NoBrush
342
+ self.brush_cursor = scene.addEllipse(
343
+ 0, 0, self.brush.size, self.brush.size, pen, brush)
344
+ self.brush_cursor.setZValue(1000)
345
+ self.brush_cursor.hide()
346
+
347
+ def update_brush_cursor(self):
348
+ if self.empty() or not self.brush_cursor or not self.isVisible():
349
+ return
350
+ self.update_cursor_pen_width()
351
+ master_view = self.get_master_view()
352
+ mouse_pos = master_view.mapFromGlobal(QCursor.pos())
353
+ if not master_view.rect().contains(mouse_pos):
354
+ self.brush_cursor.hide()
355
+ return
356
+ scene_pos = master_view.mapToScene(mouse_pos)
357
+ size = self.brush.size
358
+ radius = size / 2
359
+ self.brush_cursor.setRect(scene_pos.x() - radius, scene_pos.y() - radius, size, size)
360
+ allow_cursor_preview = self.display_manager.allow_cursor_preview()
361
+ if self.cursor_style == 'preview':
362
+ self.setup_outline_style()
363
+ if allow_cursor_preview:
364
+ self.brush_cursor.hide()
365
+ pos = QCursor.pos()
366
+ if isinstance(pos, QPointF):
367
+ scene_pos = pos
368
+ else:
369
+ cursor_pos = master_view.mapFromGlobal(pos)
370
+ scene_pos = master_view.mapToScene(cursor_pos)
371
+ self.brush_preview.update(scene_pos, int(size))
372
+ else:
373
+ self.brush_preview.hide()
374
+ if self.cursor_style == 'outline':
375
+ self.setup_outline_style()
376
+ else:
377
+ self.setup_simple_brush_style(scene_pos.x(), scene_pos.y(), radius)
378
+ if not self.brush_cursor.isVisible():
379
+ self.brush_cursor.show()
380
+
381
+ def position_on_image(self, pos):
382
+ master_view = self.get_master_view()
383
+ pixmap = self.get_master_pixmap()
384
+ scene_pos = master_view.mapToScene(pos)
385
+ item_pos = pixmap.mapFromScene(scene_pos)
386
+ return item_pos
387
+
388
+ def get_visible_image_region(self):
389
+ if self.empty():
390
+ return None
391
+ master_view = self.get_master_view()
392
+ master_pixmap = self.get_master_pixmap()
393
+ pixmap = self.get_master_pixmap()
394
+ view_rect = master_view.viewport().rect()
395
+ scene_rect = master_view.mapToScene(view_rect).boundingRect()
396
+ image_rect = master_pixmap.mapFromScene(scene_rect).boundingRect().toRect()
397
+ return image_rect.intersected(pixmap.boundingRect().toRect())
398
+
399
+ def get_visible_image_portion(self):
400
+ if self.has_no_master_layer():
401
+ return None
402
+ visible_rect = self.get_visible_image_region()
403
+ if not visible_rect:
404
+ return self.master_layer()
405
+ x, y = int(visible_rect.x()), int(visible_rect.y())
406
+ w, h = int(visible_rect.width()), int(visible_rect.height())
407
+ master_img = self.master_layer()
408
+ return master_img[y:y + h, x:x + w], (x, y, w, h)
409
+
410
+ def map_to_scene(self, pos):
411
+ return self.get_master_view().mapToScene(pos)
412
+
413
+ # pylint: disable=C0103
414
+ def keyPressEvent(self, event):
415
+ if self.empty():
416
+ return
417
+ if event.key() == Qt.Key_Space and not self.scrolling:
418
+ self.space_pressed = True
419
+ self.get_master_view().setCursor(Qt.OpenHandCursor)
420
+ if self.brush_cursor:
421
+ self.brush_cursor.hide()
422
+ if self.handle_key_press_event(event):
423
+ if event.key() == Qt.Key_Control and not self.scrolling:
424
+ self.control_pressed = True
425
+ super().keyPressEvent(event)
426
+
427
+ def keyReleaseEvent(self, event):
428
+ if self.empty():
429
+ return
430
+ self.update_brush_cursor()
431
+ if event.key() == Qt.Key_Space:
432
+ self.space_pressed = False
433
+ if not self.scrolling:
434
+ self.get_master_view().setCursor(Qt.BlankCursor)
435
+ if self.brush_cursor:
436
+ self.brush_cursor.show()
437
+ if self.handle_key_release_event(event):
438
+ if event.key() == Qt.Key_Control:
439
+ self.control_pressed = False
440
+ super().keyReleaseEvent(event)
441
+
442
+ def leaveEvent(self, event):
443
+ if self.empty():
444
+ self.setCursor(Qt.ArrowCursor)
445
+ else:
446
+ self.get_master_view().setCursor(Qt.ArrowCursor)
447
+ if self.brush_cursor:
448
+ self.brush_cursor.hide()
449
+ super().leaveEvent(event)
450
+ # pylint: enable=C0103
451
+
452
+ def scroll_view(self, view, delta_x, delta_y):
453
+ view.horizontalScrollBar().setValue(
454
+ view.horizontalScrollBar().value() - delta_x)
455
+ view.verticalScrollBar().setValue(
456
+ view.verticalScrollBar().value() - delta_y)
457
+ self.status.set_scroll(view.horizontalScrollBar().value(),
458
+ view.verticalScrollBar().value())
459
+
460
+ def center_image(self, view):
461
+ view.horizontalScrollBar().setValue(self.status.h_scroll)
462
+ view.verticalScrollBar().setValue(self.status.v_scroll)
463
+
464
+ def mouse_move_event(self, event):
465
+ if self.empty():
466
+ return
467
+ position = event.position()
468
+ brush_size = self.brush.size
469
+ if not self.space_pressed:
470
+ self.update_brush_cursor()
471
+ if self.dragging and event.buttons() & Qt.LeftButton:
472
+ current_time = QTime.currentTime()
473
+ if self.last_update_time.msecsTo(current_time) >= gui_constants.PAINT_REFRESH_TIMER:
474
+ min_step = brush_size * \
475
+ gui_constants.MIN_MOUSE_STEP_BRUSH_FRACTION * self.zoom_factor()
476
+ x, y = position.x(), position.y()
477
+ xp, yp = self.last_brush_pos.x(), self.last_brush_pos.y()
478
+ distance = math.sqrt((x - xp)**2 + (y - yp)**2)
479
+ n_steps = int(float(distance) / min_step)
480
+ if n_steps > 0:
481
+ delta_x = (position.x() - self.last_brush_pos.x()) / n_steps
482
+ delta_y = (position.y() - self.last_brush_pos.y()) / n_steps
483
+ for i in range(0, n_steps + 1):
484
+ pos = QPoint(self.last_brush_pos.x() + i * delta_x,
485
+ self.last_brush_pos.y() + i * delta_y)
486
+ self.brush_operation_continued.emit(pos)
487
+ self.last_brush_pos = position
488
+ self.last_update_time = current_time
489
+ if self.scrolling and event.buttons() & Qt.LeftButton:
490
+ master_view = self.get_master_view()
491
+ if self.space_pressed:
492
+ master_view.setCursor(Qt.ClosedHandCursor)
493
+ if self.brush_cursor:
494
+ self.brush_cursor.hide()
495
+ delta = position - self.last_mouse_pos
496
+ self.last_mouse_pos = position
497
+ self.scroll_view(master_view, delta.x(), delta.y())
498
+
499
+ def mouse_press_event(self, event):
500
+ if self.empty():
501
+ return
502
+ if event.button() == Qt.LeftButton and self.has_master_layer():
503
+ if self.space_pressed:
504
+ self.scrolling = True
505
+ self.last_mouse_pos = event.position()
506
+ self.setCursor(Qt.ClosedHandCursor)
507
+ else:
508
+ self.last_brush_pos = event.position()
509
+ self.brush_operation_started.emit(event.position().toPoint())
510
+ self.dragging = True
511
+ if self.brush_cursor:
512
+ self.brush_cursor.show()
513
+
514
+ def mouse_release_event(self, event):
515
+ if self.empty():
516
+ return
517
+ master_view = self.get_master_view()
518
+ if self.space_pressed:
519
+ master_view.setCursor(Qt.OpenHandCursor)
520
+ if self.brush_cursor:
521
+ self.brush_cursor.hide()
522
+ else:
523
+ master_view.setCursor(Qt.BlankCursor)
524
+ if self.brush_cursor:
525
+ self.brush_cursor.show()
526
+ if event.button() == Qt.LeftButton:
527
+ if self.scrolling:
528
+ self.scrolling = False
529
+ self.last_mouse_pos = None
530
+ elif self.dragging:
531
+ self.dragging = False
532
+ self.brush_operation_ended.emit()
533
+
534
+ def handle_pinch_gesture(self, pinch):
535
+ master_view = self.get_master_view()
536
+ if pinch.state() == Qt.GestureStarted:
537
+ self.pinch_start_scale = self.zoom_factor()
538
+ self.pinch_center_view = pinch.centerPoint()
539
+ self.pinch_center_scene = master_view.mapToScene(self.pinch_center_view.toPoint())
540
+ self.gesture_active = True
541
+ elif pinch.state() == Qt.GestureUpdated:
542
+ new_scale = self.pinch_start_scale * pinch.totalScaleFactor()
543
+ new_scale = max(self.min_scale(), min(new_scale, self.max_scale()))
544
+ if abs(new_scale - self.zoom_factor()) > 0.01:
545
+ self.set_zoom_factor(new_scale)
546
+ self.apply_zoom()
547
+ new_center = master_view.mapToScene(self.pinch_center_view.toPoint())
548
+ delta = self.pinch_center_scene - new_center
549
+ h_scroll = master_view.horizontalScrollBar().value() + \
550
+ int(delta.x() * self.zoom_factor())
551
+ v_scroll = master_view.verticalScrollBar().value() + \
552
+ int(delta.y() * self.zoom_factor())
553
+ self.status.set_scroll(h_scroll, v_scroll)
554
+ self.center_image(master_view)
555
+ elif pinch.state() in (Qt.GestureFinished, Qt.GestureCanceled):
556
+ self.gesture_active = False
557
+ self.update_cursor_pen_width()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.3.1
3
+ Version: 1.5.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -70,11 +70,11 @@ The GUI has two main working areas:
70
70
 
71
71
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
72
72
 
73
- # Resources
73
+ ## Resources
74
74
 
75
75
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
76
76
 
77
- # Note for macOS users
77
+ ## Note for macOS users
78
78
 
79
79
  **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
80
80
 
@@ -93,17 +93,17 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
93
93
 
94
94
  macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
95
95
 
96
- # Credits
96
+ ## Credits
97
97
 
98
98
  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.
99
99
 
100
- # Resources
100
+ ## Resources
101
101
 
102
102
  * [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
103
103
  Pyramid methods in image processing
104
104
  * [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
105
105
 
106
- # License
106
+ ## License
107
107
 
108
108
  <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
109
109
 
@@ -112,7 +112,7 @@ Pyramid methods in image processing
112
112
 
113
113
  - **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
114
114
 
115
- # Attribution request
115
+ ## Attribution request
116
116
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
117
117
 
118
118
  *Created with Shine Stacker – https://github.com/lucalista/shinestacker*