setiastrosuitepro 1.6.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 setiastrosuitepro might be problematic. Click here for more details.

Files changed (174) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/saspro/__init__.py +20 -0
  3. setiastro/saspro/__main__.py +784 -0
  4. setiastro/saspro/_generated/__init__.py +7 -0
  5. setiastro/saspro/_generated/build_info.py +2 -0
  6. setiastro/saspro/abe.py +1295 -0
  7. setiastro/saspro/abe_preset.py +196 -0
  8. setiastro/saspro/aberration_ai.py +694 -0
  9. setiastro/saspro/aberration_ai_preset.py +224 -0
  10. setiastro/saspro/accel_installer.py +218 -0
  11. setiastro/saspro/accel_workers.py +30 -0
  12. setiastro/saspro/add_stars.py +621 -0
  13. setiastro/saspro/astrobin_exporter.py +1007 -0
  14. setiastro/saspro/astrospike.py +153 -0
  15. setiastro/saspro/astrospike_python.py +1839 -0
  16. setiastro/saspro/autostretch.py +196 -0
  17. setiastro/saspro/backgroundneutral.py +560 -0
  18. setiastro/saspro/batch_convert.py +325 -0
  19. setiastro/saspro/batch_renamer.py +519 -0
  20. setiastro/saspro/blemish_blaster.py +488 -0
  21. setiastro/saspro/blink_comparator_pro.py +2923 -0
  22. setiastro/saspro/bundles.py +61 -0
  23. setiastro/saspro/bundles_dock.py +114 -0
  24. setiastro/saspro/cheat_sheet.py +168 -0
  25. setiastro/saspro/clahe.py +342 -0
  26. setiastro/saspro/comet_stacking.py +1377 -0
  27. setiastro/saspro/config.py +38 -0
  28. setiastro/saspro/config_bootstrap.py +40 -0
  29. setiastro/saspro/config_manager.py +316 -0
  30. setiastro/saspro/continuum_subtract.py +1617 -0
  31. setiastro/saspro/convo.py +1397 -0
  32. setiastro/saspro/convo_preset.py +414 -0
  33. setiastro/saspro/copyastro.py +187 -0
  34. setiastro/saspro/cosmicclarity.py +1564 -0
  35. setiastro/saspro/cosmicclarity_preset.py +407 -0
  36. setiastro/saspro/crop_dialog_pro.py +948 -0
  37. setiastro/saspro/crop_preset.py +189 -0
  38. setiastro/saspro/curve_editor_pro.py +2544 -0
  39. setiastro/saspro/curves_preset.py +375 -0
  40. setiastro/saspro/debayer.py +670 -0
  41. setiastro/saspro/debug_utils.py +29 -0
  42. setiastro/saspro/dnd_mime.py +35 -0
  43. setiastro/saspro/doc_manager.py +2634 -0
  44. setiastro/saspro/exoplanet_detector.py +2166 -0
  45. setiastro/saspro/file_utils.py +284 -0
  46. setiastro/saspro/fitsmodifier.py +744 -0
  47. setiastro/saspro/free_torch_memory.py +48 -0
  48. setiastro/saspro/frequency_separation.py +1343 -0
  49. setiastro/saspro/function_bundle.py +1594 -0
  50. setiastro/saspro/ghs_dialog_pro.py +660 -0
  51. setiastro/saspro/ghs_preset.py +284 -0
  52. setiastro/saspro/graxpert.py +634 -0
  53. setiastro/saspro/graxpert_preset.py +287 -0
  54. setiastro/saspro/gui/__init__.py +0 -0
  55. setiastro/saspro/gui/main_window.py +8494 -0
  56. setiastro/saspro/gui/mixins/__init__.py +33 -0
  57. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  58. setiastro/saspro/gui/mixins/file_mixin.py +445 -0
  59. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  60. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  61. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  62. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  63. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  64. setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
  65. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  66. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  67. setiastro/saspro/halobgon.py +462 -0
  68. setiastro/saspro/header_viewer.py +445 -0
  69. setiastro/saspro/headless_utils.py +88 -0
  70. setiastro/saspro/histogram.py +753 -0
  71. setiastro/saspro/history_explorer.py +939 -0
  72. setiastro/saspro/image_combine.py +414 -0
  73. setiastro/saspro/image_peeker_pro.py +1596 -0
  74. setiastro/saspro/imageops/__init__.py +37 -0
  75. setiastro/saspro/imageops/mdi_snap.py +292 -0
  76. setiastro/saspro/imageops/scnr.py +36 -0
  77. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  78. setiastro/saspro/imageops/stretch.py +244 -0
  79. setiastro/saspro/isophote.py +1179 -0
  80. setiastro/saspro/layers.py +208 -0
  81. setiastro/saspro/layers_dock.py +714 -0
  82. setiastro/saspro/lazy_imports.py +193 -0
  83. setiastro/saspro/legacy/__init__.py +2 -0
  84. setiastro/saspro/legacy/image_manager.py +2226 -0
  85. setiastro/saspro/legacy/numba_utils.py +3659 -0
  86. setiastro/saspro/legacy/xisf.py +1071 -0
  87. setiastro/saspro/linear_fit.py +534 -0
  88. setiastro/saspro/live_stacking.py +1830 -0
  89. setiastro/saspro/log_bus.py +5 -0
  90. setiastro/saspro/logging_config.py +460 -0
  91. setiastro/saspro/luminancerecombine.py +309 -0
  92. setiastro/saspro/main_helpers.py +201 -0
  93. setiastro/saspro/mask_creation.py +928 -0
  94. setiastro/saspro/masks_core.py +56 -0
  95. setiastro/saspro/mdi_widgets.py +353 -0
  96. setiastro/saspro/memory_utils.py +666 -0
  97. setiastro/saspro/metadata_patcher.py +75 -0
  98. setiastro/saspro/mfdeconv.py +3826 -0
  99. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  100. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  101. setiastro/saspro/mfdeconvsport.py +2382 -0
  102. setiastro/saspro/minorbodycatalog.py +567 -0
  103. setiastro/saspro/morphology.py +382 -0
  104. setiastro/saspro/multiscale_decomp.py +1290 -0
  105. setiastro/saspro/nbtorgb_stars.py +531 -0
  106. setiastro/saspro/numba_utils.py +3044 -0
  107. setiastro/saspro/numba_warmup.py +141 -0
  108. setiastro/saspro/ops/__init__.py +9 -0
  109. setiastro/saspro/ops/command_help_dialog.py +623 -0
  110. setiastro/saspro/ops/command_runner.py +217 -0
  111. setiastro/saspro/ops/commands.py +1594 -0
  112. setiastro/saspro/ops/script_editor.py +1102 -0
  113. setiastro/saspro/ops/scripts.py +1413 -0
  114. setiastro/saspro/ops/settings.py +560 -0
  115. setiastro/saspro/parallel_utils.py +554 -0
  116. setiastro/saspro/pedestal.py +121 -0
  117. setiastro/saspro/perfect_palette_picker.py +1053 -0
  118. setiastro/saspro/pipeline.py +110 -0
  119. setiastro/saspro/pixelmath.py +1600 -0
  120. setiastro/saspro/plate_solver.py +2435 -0
  121. setiastro/saspro/project_io.py +797 -0
  122. setiastro/saspro/psf_utils.py +136 -0
  123. setiastro/saspro/psf_viewer.py +549 -0
  124. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  125. setiastro/saspro/remove_green.py +314 -0
  126. setiastro/saspro/remove_stars.py +1625 -0
  127. setiastro/saspro/remove_stars_preset.py +404 -0
  128. setiastro/saspro/resources.py +472 -0
  129. setiastro/saspro/rgb_combination.py +207 -0
  130. setiastro/saspro/rgb_extract.py +19 -0
  131. setiastro/saspro/rgbalign.py +723 -0
  132. setiastro/saspro/runtime_imports.py +7 -0
  133. setiastro/saspro/runtime_torch.py +754 -0
  134. setiastro/saspro/save_options.py +72 -0
  135. setiastro/saspro/selective_color.py +1552 -0
  136. setiastro/saspro/sfcc.py +1425 -0
  137. setiastro/saspro/shortcuts.py +2807 -0
  138. setiastro/saspro/signature_insert.py +1099 -0
  139. setiastro/saspro/stacking_suite.py +17712 -0
  140. setiastro/saspro/star_alignment.py +7420 -0
  141. setiastro/saspro/star_alignment_preset.py +329 -0
  142. setiastro/saspro/star_metrics.py +49 -0
  143. setiastro/saspro/star_spikes.py +681 -0
  144. setiastro/saspro/star_stretch.py +470 -0
  145. setiastro/saspro/stat_stretch.py +502 -0
  146. setiastro/saspro/status_log_dock.py +78 -0
  147. setiastro/saspro/subwindow.py +3267 -0
  148. setiastro/saspro/supernovaasteroidhunter.py +1712 -0
  149. setiastro/saspro/swap_manager.py +99 -0
  150. setiastro/saspro/torch_backend.py +89 -0
  151. setiastro/saspro/torch_rejection.py +434 -0
  152. setiastro/saspro/view_bundle.py +1555 -0
  153. setiastro/saspro/wavescale_hdr.py +624 -0
  154. setiastro/saspro/wavescale_hdr_preset.py +100 -0
  155. setiastro/saspro/wavescalede.py +657 -0
  156. setiastro/saspro/wavescalede_preset.py +228 -0
  157. setiastro/saspro/wcs_update.py +374 -0
  158. setiastro/saspro/whitebalance.py +456 -0
  159. setiastro/saspro/widgets/__init__.py +48 -0
  160. setiastro/saspro/widgets/common_utilities.py +305 -0
  161. setiastro/saspro/widgets/graphics_views.py +122 -0
  162. setiastro/saspro/widgets/image_utils.py +518 -0
  163. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  164. setiastro/saspro/widgets/spinboxes.py +275 -0
  165. setiastro/saspro/widgets/themed_buttons.py +13 -0
  166. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  167. setiastro/saspro/window_shelf.py +185 -0
  168. setiastro/saspro/xisf.py +1123 -0
  169. setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
  170. setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
  171. setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
  172. setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
  173. setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
  174. setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,305 @@
1
+ """
2
+ Common UI utilities and shared components.
3
+
4
+ This module provides centralized implementations of commonly used
5
+ UI components and utilities to avoid code duplication across the codebase.
6
+
7
+ Included:
8
+ - AboutDialog: Standard about dialog
9
+ - ProjectSaveWorker: Background thread for saving projects
10
+ - install_crash_handlers: Global exception/crash handling setup
11
+ - UI decoration helpers: DECOR_GLYPHS, strip_ui_decorations
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ import os
18
+ import threading
19
+ import traceback
20
+ import logging
21
+ import atexit
22
+ from typing import TYPE_CHECKING, Optional, List, Any
23
+
24
+ from PyQt6.QtCore import QThread, pyqtSignal, QTimer, Qt
25
+ from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QMessageBox
26
+
27
+ if TYPE_CHECKING:
28
+ from PyQt6.QtWidgets import QApplication, QMdiArea
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Version and Build Info (should be imported from main module)
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def get_version() -> str:
36
+ """Get application version from main module."""
37
+ try:
38
+ from setiastrosuitepro import VERSION
39
+ return VERSION
40
+ except ImportError:
41
+ return "Unknown"
42
+
43
+
44
+ def get_build_timestamp() -> str:
45
+ """Get human-friendly build timestamp from main module."""
46
+ try:
47
+ from setiastrosuitepro import BUILD_TIMESTAMP
48
+ except ImportError:
49
+ return "Unknown"
50
+
51
+ if BUILD_TIMESTAMP == "dev":
52
+ # No generated build_info → running from local source checkout
53
+ return "Running locally from source code"
54
+ return BUILD_TIMESTAMP
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # About Dialog
59
+ # ---------------------------------------------------------------------------
60
+
61
+ class AboutDialog(QDialog):
62
+ """
63
+ Standard About dialog for Seti Astro Suite.
64
+ """
65
+
66
+ def __init__(self, parent: Optional[Any] = None, version: str = "", build_timestamp: str = ""):
67
+ super().__init__(parent)
68
+ self.setWindowTitle("About Seti Astro Suite")
69
+
70
+ # Get version info if not provided
71
+ if not version:
72
+ version = get_version()
73
+
74
+ # Normalize build_timestamp
75
+ if not build_timestamp:
76
+ build_timestamp = get_build_timestamp()
77
+ else:
78
+ # If someone passed the raw sentinel from the main module
79
+ if build_timestamp == "dev":
80
+ build_timestamp = "Running locally from source code"
81
+
82
+ layout = QVBoxLayout()
83
+
84
+ # Build about text with optional build timestamp
85
+ about_lines = [
86
+ f"<h2>Seti Astro's Suite Pro {version}</h2>",
87
+ "<p>Written by Franklin Marek</p>",
88
+ "<p>Collaborators: Fabio Tempera</p>",
89
+ "<p>Copyright © 2025 Seti Astro</p>",
90
+ ]
91
+
92
+ if build_timestamp and build_timestamp != "Unknown":
93
+ about_lines.append(f"<p><b>Build:</b> {build_timestamp}</p>")
94
+
95
+ about_lines.extend([
96
+ "<p>Website: <a href='http://www.setiastro.com'>www.setiastro.com</a></p>",
97
+ "<p>Donations: <a href='https://www.setiastro.com/checkout/donate?donatePageId=65ae7e7bac20370d8c04c1ab'>Click here to donate</a></p>",
98
+ ])
99
+
100
+ about_text = "".join(about_lines)
101
+
102
+ label = QLabel(about_text)
103
+ label.setTextFormat(Qt.TextFormat.RichText)
104
+ label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
105
+ label.setOpenExternalLinks(True)
106
+
107
+ layout.addWidget(label)
108
+ self.setLayout(layout)
109
+
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Project Save Worker
114
+ # ---------------------------------------------------------------------------
115
+
116
+ class ProjectSaveWorker(QThread):
117
+ """
118
+ Background thread for saving projects.
119
+
120
+ Emits 'ok' signal on success, 'error' signal with message on failure.
121
+
122
+ Args:
123
+ path: Path to save the project to
124
+ docs: List of documents to save
125
+ shortcuts: Shortcuts configuration
126
+ mdi: MDI area reference
127
+ compress: Whether to compress the project
128
+ window_shelf: Optional window shelf reference
129
+ parent: Parent QObject
130
+
131
+ Example:
132
+ worker = ProjectSaveWorker(path, docs, shortcuts, mdi, compress=True)
133
+ worker.ok.connect(on_save_complete)
134
+ worker.error.connect(on_save_error)
135
+ worker.start()
136
+ """
137
+
138
+ ok = pyqtSignal()
139
+ error = pyqtSignal(str)
140
+
141
+ def __init__(
142
+ self,
143
+ path: str,
144
+ docs: List[Any],
145
+ shortcuts: Any,
146
+ mdi: 'QMdiArea',
147
+ compress: bool,
148
+ window_shelf: Optional[Any] = None,
149
+ parent: Optional[Any] = None
150
+ ):
151
+ super().__init__(parent)
152
+ self.path = path
153
+ self.docs = docs
154
+ self.shortcuts = shortcuts
155
+ self.mdi = mdi
156
+ self.compress = compress
157
+ self.window_shelf = window_shelf
158
+
159
+ def run(self) -> None:
160
+ """Execute the save operation in background thread."""
161
+ try:
162
+ from setiastro.saspro.project_io import ProjectWriter
163
+ ProjectWriter.write(
164
+ self.path,
165
+ docs=self.docs,
166
+ shortcuts=self.shortcuts,
167
+ mdi=self.mdi,
168
+ compress=self.compress,
169
+ shelf=self.window_shelf,
170
+ )
171
+ self.ok.emit()
172
+ except Exception as e:
173
+ self.error.emit(str(e))
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # UI Decoration Helpers
178
+ # ---------------------------------------------------------------------------
179
+
180
+ DECOR_GLYPHS = "■●◆▲▪▫•◼◻◾◽"
181
+
182
+ def strip_ui_decorations(s: str) -> str:
183
+ """
184
+ Remove UI decoration glyphs and prefixes from a string.
185
+
186
+ Strips:
187
+ - Leading glyph characters followed by space
188
+ - "Active View: " prefix
189
+
190
+ Args:
191
+ s: String to clean
192
+
193
+ Returns:
194
+ Cleaned string without decorations
195
+
196
+ Example:
197
+ >>> strip_ui_decorations("● Active View: My Image.fits")
198
+ 'My Image.fits'
199
+ """
200
+ s = s or ""
201
+
202
+ # Strip any number of leading glyph+space
203
+ while len(s) >= 2 and s[1] == " " and s[0] in DECOR_GLYPHS:
204
+ s = s[2:]
205
+
206
+ # Strip leading Active prefix if present
207
+ ACTIVE = "Active View: "
208
+ if s.startswith(ACTIVE):
209
+ s = s[len(ACTIVE):]
210
+
211
+ return s
212
+
213
+
214
+ # Alias for backward compatibility
215
+ _strip_ui_decorations = strip_ui_decorations
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Crash/Exception Handlers
220
+ # ---------------------------------------------------------------------------
221
+
222
+ def install_crash_handlers(app: 'QApplication') -> None:
223
+ """
224
+ Install global crash and exception handlers for the application.
225
+
226
+ Sets up:
227
+ 1. faulthandler for hard crashes (segfaults) → saspro_crash.log
228
+ 2. sys.excepthook for uncaught main thread exceptions
229
+ 3. threading.excepthook for uncaught background thread exceptions
230
+
231
+ All exceptions are logged and displayed in a dialog to the user.
232
+
233
+ Args:
234
+ app: The QApplication instance
235
+
236
+ Example:
237
+ app = QApplication(sys.argv)
238
+ install_crash_handlers(app)
239
+ """
240
+ import faulthandler
241
+
242
+ # 1) Hard crashes (segfaults, access violations) → saspro_crash.log
243
+ try:
244
+ _crash_log = open("saspro_crash.log", "w", encoding="utf-8", errors="replace")
245
+ faulthandler.enable(file=_crash_log, all_threads=True)
246
+ atexit.register(_crash_log.close)
247
+ except Exception:
248
+ logging.exception("Failed to enable faulthandler")
249
+
250
+ def _show_dialog(title: str, head: str, details: str) -> None:
251
+ """Show error dialog marshaled to main thread."""
252
+ def _ui():
253
+ m = QMessageBox(app.activeWindow())
254
+ m.setIcon(QMessageBox.Icon.Critical)
255
+ m.setWindowTitle(title)
256
+ m.setText(head)
257
+ m.setInformativeText("Details are available below and in saspro.log.")
258
+ if details:
259
+ m.setDetailedText(details)
260
+ m.setStandardButtons(QMessageBox.StandardButton.Ok)
261
+ m.exec()
262
+ QTimer.singleShot(0, _ui)
263
+
264
+ # 2) Any uncaught exception on the main thread
265
+ def _excepthook(exc_type, exc_value, exc_tb):
266
+ tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
267
+ logging.error("Uncaught exception:\n%s", tb)
268
+ _show_dialog(
269
+ "Unhandled Exception",
270
+ f"{exc_type.__name__}: {exc_value}",
271
+ tb
272
+ )
273
+
274
+ sys.excepthook = _excepthook
275
+
276
+ # 3) Any uncaught exception in background threads (Py3.8+)
277
+ def _threadhook(args: threading.ExceptHookArgs):
278
+ tb = "".join(traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback))
279
+ logging.error("Uncaught thread exception (%s):\n%s", args.thread.name, tb)
280
+ _show_dialog(
281
+ "Unhandled Thread Exception",
282
+ f"{args.exc_type.__name__}: {args.exc_value}",
283
+ tb
284
+ )
285
+
286
+ try:
287
+ threading.excepthook = _threadhook # type: ignore[attr-defined]
288
+ except Exception:
289
+ pass
290
+
291
+
292
+ # ---------------------------------------------------------------------------
293
+ # Module exports
294
+ # ---------------------------------------------------------------------------
295
+
296
+ __all__ = [
297
+ 'AboutDialog',
298
+ 'ProjectSaveWorker',
299
+ 'DECOR_GLYPHS',
300
+ 'strip_ui_decorations',
301
+ '_strip_ui_decorations', # Backward compatibility
302
+ 'install_crash_handlers',
303
+ 'get_version',
304
+ 'get_build_timestamp',
305
+ ]
@@ -0,0 +1,122 @@
1
+ # pro/widgets/graphics_views.py
2
+ """
3
+ Centralized graphics view widgets for Seti Astro Suite Pro.
4
+
5
+ Provides reusable zoomable and interactive QGraphicsView widgets.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from PyQt6.QtCore import Qt
10
+ from PyQt6.QtGui import QWheelEvent, QPainter
11
+ from PyQt6.QtWidgets import QGraphicsView
12
+
13
+
14
+ class ZoomableGraphicsView(QGraphicsView):
15
+ """
16
+ A QGraphicsView with mouse wheel zoom (Ctrl+wheel) and drag support.
17
+
18
+ Features:
19
+ - Ctrl+wheel to zoom in/out
20
+ - Scroll hand drag mode
21
+ - Smooth pixmap transform
22
+ - Configurable zoom limits and step
23
+
24
+ Usage:
25
+ view = ZoomableGraphicsView()
26
+ view.setScene(scene)
27
+ view.zoom_in()
28
+ view.zoom_out()
29
+ view.fit_to_item(pixmap_item)
30
+ """
31
+
32
+ def __init__(self, scene=None, parent=None, *,
33
+ zoom_min: float = 0.05,
34
+ zoom_max: float = 12.0,
35
+ zoom_step: float = 1.25):
36
+ super().__init__(parent)
37
+ if scene is not None:
38
+ self.setScene(scene)
39
+
40
+ self._zoom = 1.0
41
+ self._zoom_min = zoom_min
42
+ self._zoom_max = zoom_max
43
+ self._zoom_step = zoom_step
44
+
45
+ # Default configuration
46
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
47
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
48
+ self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
49
+ self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
50
+ self.setRenderHint(QPainter.RenderHint.Antialiasing, True)
51
+
52
+ def wheelEvent(self, event: QWheelEvent):
53
+ """Handle wheel events - zoom with Ctrl modifier."""
54
+ if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
55
+ delta = event.angleDelta().y()
56
+ if delta == 0:
57
+ event.accept()
58
+ return
59
+ self._apply_zoom(
60
+ up=(delta > 0),
61
+ anchor=QGraphicsView.ViewportAnchor.AnchorUnderMouse
62
+ )
63
+ event.accept()
64
+ else:
65
+ super().wheelEvent(event)
66
+
67
+ def _apply_zoom(self, up: bool, anchor: QGraphicsView.ViewportAnchor | None = None):
68
+ """Apply zoom in/out with optional anchor point."""
69
+ old_anchor = self.transformationAnchor()
70
+ if anchor is not None:
71
+ self.setTransformationAnchor(anchor)
72
+
73
+ step = self._zoom_step if up else (1.0 / self._zoom_step)
74
+ new_zoom = max(self._zoom_min, min(self._zoom_max, self._zoom * step))
75
+ factor = new_zoom / self._zoom
76
+
77
+ if factor != 1.0:
78
+ self.scale(factor, factor)
79
+ self._zoom = new_zoom
80
+
81
+ if anchor is not None:
82
+ self.setTransformationAnchor(old_anchor)
83
+
84
+ def zoom_in(self):
85
+ """Zoom in centered on the view."""
86
+ self._apply_zoom(True, anchor=QGraphicsView.ViewportAnchor.AnchorViewCenter)
87
+
88
+ def zoom_out(self):
89
+ """Zoom out centered on the view."""
90
+ self._apply_zoom(False, anchor=QGraphicsView.ViewportAnchor.AnchorViewCenter)
91
+
92
+ def fit_to_item(self, item):
93
+ """Fit the view to show the entire item."""
94
+ if item is None:
95
+ return
96
+ # Handle both pixmap items and generic items
97
+ if hasattr(item, 'pixmap') and item.pixmap().isNull():
98
+ return
99
+ self._zoom = 1.0
100
+ self.resetTransform()
101
+ self.fitInView(item, Qt.AspectRatioMode.KeepAspectRatio)
102
+
103
+ # Alias for compatibility
104
+ fit_item = fit_to_item
105
+
106
+ @property
107
+ def zoom_level(self) -> float:
108
+ """Get current zoom level."""
109
+ return self._zoom
110
+
111
+ def set_zoom(self, level: float):
112
+ """Set zoom to a specific level."""
113
+ level = max(self._zoom_min, min(self._zoom_max, level))
114
+ factor = level / self._zoom
115
+ if factor != 1.0:
116
+ self.scale(factor, factor)
117
+ self._zoom = level
118
+
119
+ def reset_zoom(self):
120
+ """Reset zoom to 1:1."""
121
+ self.resetTransform()
122
+ self._zoom = 1.0