setiastrosuitepro 1.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/__init__.py +2 -0
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +809 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2926 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +178 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +956 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2641 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +745 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/generate_translations.py +2378 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8567 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +443 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/i18n.py +156 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1601 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +679 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1070 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2444 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +477 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1430 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +18181 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +506 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1716 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/de_translations.py +3733 -0
- setiastro/saspro/translations/es_translations.py +3923 -0
- setiastro/saspro/translations/fr_translations.py +3842 -0
- setiastro/saspro/translations/integrate_translations.py +234 -0
- setiastro/saspro/translations/it_translations.py +3662 -0
- setiastro/saspro/translations/ja_translations.py +3585 -0
- setiastro/saspro/translations/pt_translations.py +3853 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/zh_translations.py +3659 -0
- setiastro/saspro/versioning.py +71 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +658 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
- setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
- setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# pro/widgets/preview_dialogs.py
|
|
2
|
+
"""
|
|
3
|
+
Centralized preview dialog widgets for Seti Astro Suite Pro.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from PyQt6.QtCore import Qt, QTimer, QPoint, QEvent
|
|
9
|
+
from PyQt6.QtGui import QImage, QPixmap, QIcon
|
|
10
|
+
from PyQt6.QtWidgets import (
|
|
11
|
+
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
|
|
12
|
+
QScrollArea, QToolButton
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
16
|
+
|
|
17
|
+
# Import stretch functions - these are core to the app
|
|
18
|
+
from setiastro.saspro.imageops.stretch import stretch_mono_image, stretch_color_image
|
|
19
|
+
|
|
20
|
+
def _as_float01(img: np.ndarray) -> np.ndarray:
|
|
21
|
+
a = np.asarray(img)
|
|
22
|
+
|
|
23
|
+
# Integers: scale by dtype max
|
|
24
|
+
if a.dtype.kind in "ui":
|
|
25
|
+
maxv = float(np.iinfo(a.dtype).max)
|
|
26
|
+
if maxv <= 0:
|
|
27
|
+
return np.zeros_like(a, dtype=np.float32)
|
|
28
|
+
return (a.astype(np.float32) / maxv).clip(0.0, 1.0)
|
|
29
|
+
|
|
30
|
+
# Floats: if already 0..1, keep; otherwise scale down
|
|
31
|
+
af = a.astype(np.float32, copy=False)
|
|
32
|
+
|
|
33
|
+
mx = float(np.nanmax(af)) if af.size else 0.0
|
|
34
|
+
if not np.isfinite(mx) or mx <= 1.0:
|
|
35
|
+
return np.clip(af, 0.0, 1.0)
|
|
36
|
+
|
|
37
|
+
# Common “display buffers”: 0..255 or 0..65535 stored as float
|
|
38
|
+
if mx <= 255.5:
|
|
39
|
+
return np.clip(af / 255.0, 0.0, 1.0)
|
|
40
|
+
if mx <= 65535.5:
|
|
41
|
+
return np.clip(af / 65535.0, 0.0, 1.0)
|
|
42
|
+
|
|
43
|
+
# Fallback: normalize by max
|
|
44
|
+
return np.clip(af / mx, 0.0, 1.0)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ImagePreviewDialog(QDialog):
|
|
48
|
+
"""
|
|
49
|
+
A dialog for previewing images with autostretch and zoom capabilities.
|
|
50
|
+
|
|
51
|
+
Features:
|
|
52
|
+
- AutoStretch toggle
|
|
53
|
+
- Zoom in/out (buttons and mouse wheel)
|
|
54
|
+
- Scroll area for panning
|
|
55
|
+
- Supports mono and RGB images
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
dialog = ImagePreviewDialog(np_image, is_mono=False)
|
|
59
|
+
dialog.exec()
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, np_image: np.ndarray, is_mono: bool = False, parent=None):
|
|
63
|
+
super().__init__(parent)
|
|
64
|
+
self.setWindowTitle(self.tr("Image Preview"))
|
|
65
|
+
self.resize(640, 480)
|
|
66
|
+
|
|
67
|
+
self.autostretch_enabled = False
|
|
68
|
+
self.is_mono = is_mono
|
|
69
|
+
self.zoom_factor = 1.0
|
|
70
|
+
# Drag-to-pan state
|
|
71
|
+
self._panning = False
|
|
72
|
+
self._pan_last = QPoint()
|
|
73
|
+
|
|
74
|
+
# Store the image, ensure float32 [0,1]
|
|
75
|
+
self.np_image = _as_float01(np_image)
|
|
76
|
+
|
|
77
|
+
# Store the image, ensure float32 [0,1]
|
|
78
|
+
self.np_image = _as_float01(np_image)
|
|
79
|
+
|
|
80
|
+
# Layout
|
|
81
|
+
layout = QVBoxLayout(self)
|
|
82
|
+
|
|
83
|
+
# Toolbar row (themed)
|
|
84
|
+
bar = QHBoxLayout()
|
|
85
|
+
|
|
86
|
+
self.autostretch_button = QToolButton()
|
|
87
|
+
self.autostretch_button.setText(self.tr("AutoStretch (Off)"))
|
|
88
|
+
self.autostretch_button.setToolTip(self.tr("Toggle AutoStretch"))
|
|
89
|
+
self.autostretch_button.setCheckable(True)
|
|
90
|
+
self.autostretch_button.toggled.connect(self._toggle_autostretch)
|
|
91
|
+
bar.addWidget(self.autostretch_button)
|
|
92
|
+
|
|
93
|
+
bar.addStretch(1)
|
|
94
|
+
|
|
95
|
+
self.zoom_in_button = themed_toolbtn("zoom-in", self.tr("Zoom In"))
|
|
96
|
+
self.zoom_out_button = themed_toolbtn("zoom-out", self.tr("Zoom Out"))
|
|
97
|
+
self.zoom_1to1_button = themed_toolbtn("zoom-original", self.tr("1:1 (100%)"))
|
|
98
|
+
self.fit_button = themed_toolbtn("zoom-fit-best", self.tr("Fit to Preview"))
|
|
99
|
+
|
|
100
|
+
self.zoom_in_button.clicked.connect(self._zoom_in)
|
|
101
|
+
self.zoom_out_button.clicked.connect(self._zoom_out)
|
|
102
|
+
self.zoom_1to1_button.clicked.connect(self._one_to_one)
|
|
103
|
+
self.fit_button.clicked.connect(self._fit_to_preview)
|
|
104
|
+
|
|
105
|
+
bar.addWidget(self.zoom_in_button)
|
|
106
|
+
bar.addWidget(self.zoom_out_button)
|
|
107
|
+
bar.addWidget(self.zoom_1to1_button)
|
|
108
|
+
bar.addWidget(self.fit_button)
|
|
109
|
+
|
|
110
|
+
layout.addLayout(bar)
|
|
111
|
+
|
|
112
|
+
# Scroll area
|
|
113
|
+
self.scroll_area = QScrollArea(self)
|
|
114
|
+
self.scroll_area.setWidgetResizable(True)
|
|
115
|
+
layout.addWidget(self.scroll_area)
|
|
116
|
+
|
|
117
|
+
# Image label
|
|
118
|
+
self.image_label = QLabel()
|
|
119
|
+
self.scroll_area.setWidget(self.image_label)
|
|
120
|
+
self.image_label.installEventFilter(self)
|
|
121
|
+
self.scroll_area.viewport().installEventFilter(self)
|
|
122
|
+
self.image_label.setText("")
|
|
123
|
+
self.image_label.setMouseTracking(True)
|
|
124
|
+
# Display initial image
|
|
125
|
+
self._display_image(self.np_image)
|
|
126
|
+
|
|
127
|
+
# Enable mouse wheel zoom
|
|
128
|
+
self.image_label.installEventFilter(self)
|
|
129
|
+
|
|
130
|
+
# Center scrollbars after layout
|
|
131
|
+
QTimer.singleShot(0, self._fit_to_preview)
|
|
132
|
+
|
|
133
|
+
def _display_image(self, np_img: np.ndarray):
|
|
134
|
+
"""Convert numpy array to QImage and display at current zoom."""
|
|
135
|
+
# Convert to uint8
|
|
136
|
+
arr = (np.clip(np_img, 0, 1) * 255).astype(np.uint8)
|
|
137
|
+
|
|
138
|
+
if arr.ndim == 3 and arr.shape[2] == 3:
|
|
139
|
+
h, w, _ = arr.shape
|
|
140
|
+
qimg = QImage(arr.tobytes(), w, h, 3 * w, QImage.Format.Format_RGB888)
|
|
141
|
+
elif arr.ndim == 2:
|
|
142
|
+
h, w = arr.shape
|
|
143
|
+
qimg = QImage(arr.tobytes(), w, h, w, QImage.Format.Format_Grayscale8)
|
|
144
|
+
elif arr.ndim == 3 and arr.shape[2] == 1:
|
|
145
|
+
arr = arr.squeeze()
|
|
146
|
+
h, w = arr.shape
|
|
147
|
+
qimg = QImage(arr.tobytes(), w, h, w, QImage.Format.Format_Grayscale8)
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError(f"Unexpected image shape: {arr.shape}")
|
|
150
|
+
|
|
151
|
+
# Apply zoom
|
|
152
|
+
pixmap = QPixmap.fromImage(qimg)
|
|
153
|
+
scaled_w = int(pixmap.width() * self.zoom_factor)
|
|
154
|
+
scaled_h = int(pixmap.height() * self.zoom_factor)
|
|
155
|
+
scaled_pixmap = pixmap.scaled(
|
|
156
|
+
scaled_w, scaled_h,
|
|
157
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
158
|
+
Qt.TransformationMode.SmoothTransformation
|
|
159
|
+
)
|
|
160
|
+
self.image_label.setPixmap(scaled_pixmap)
|
|
161
|
+
self.image_label.adjustSize()
|
|
162
|
+
|
|
163
|
+
def _toggle_autostretch(self, checked: bool):
|
|
164
|
+
"""Toggle autostretch on/off."""
|
|
165
|
+
self.autostretch_enabled = checked
|
|
166
|
+
self.autostretch_button.setText(
|
|
167
|
+
self.tr("AutoStretch (On)") if checked else self.tr("AutoStretch (Off)")
|
|
168
|
+
)
|
|
169
|
+
self._apply_display()
|
|
170
|
+
|
|
171
|
+
def _one_to_one(self):
|
|
172
|
+
self.zoom_factor = 1.0
|
|
173
|
+
self._apply_display()
|
|
174
|
+
|
|
175
|
+
def _fit_to_preview(self):
|
|
176
|
+
# Fit image into the scroll viewport
|
|
177
|
+
if self.image_label.pixmap() is None or self.image_label.pixmap().isNull():
|
|
178
|
+
return
|
|
179
|
+
vp = self.scroll_area.viewport().size()
|
|
180
|
+
pm = self.image_label.pixmap()
|
|
181
|
+
if pm.width() <= 0 or pm.height() <= 0:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Compute zoom that fits current *source image* (not already scaled label)
|
|
185
|
+
# So, recompute based on original image dims:
|
|
186
|
+
base_h, base_w = self.np_image.shape[:2]
|
|
187
|
+
if base_w <= 0 or base_h <= 0:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
zx = vp.width() / float(base_w)
|
|
191
|
+
zy = vp.height() / float(base_h)
|
|
192
|
+
self.zoom_factor = max(0.01, min(zx, zy))
|
|
193
|
+
self._apply_display()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _apply_display(self):
|
|
197
|
+
"""Apply current display settings (autostretch, zoom)."""
|
|
198
|
+
target_median = 0.25
|
|
199
|
+
|
|
200
|
+
if self.autostretch_enabled:
|
|
201
|
+
if self.np_image.ndim == 2:
|
|
202
|
+
stretched = stretch_mono_image(self.np_image, target_median)
|
|
203
|
+
display_img = np.stack([stretched] * 3, axis=-1)
|
|
204
|
+
elif self.np_image.ndim == 3 and self.np_image.shape[2] == 3:
|
|
205
|
+
display_img = stretch_color_image(self.np_image, target_median, linked=False)
|
|
206
|
+
elif self.np_image.ndim == 3 and self.np_image.shape[2] == 1:
|
|
207
|
+
stretched = stretch_mono_image(self.np_image.squeeze(), target_median)
|
|
208
|
+
display_img = np.stack([stretched] * 3, axis=-1)
|
|
209
|
+
else:
|
|
210
|
+
display_img = self.np_image
|
|
211
|
+
else:
|
|
212
|
+
if self.np_image.ndim == 2:
|
|
213
|
+
display_img = np.stack([self.np_image] * 3, axis=-1)
|
|
214
|
+
elif self.np_image.ndim == 3 and self.np_image.shape[2] == 1:
|
|
215
|
+
display_img = np.repeat(self.np_image, 3, axis=2)
|
|
216
|
+
else:
|
|
217
|
+
display_img = self.np_image
|
|
218
|
+
|
|
219
|
+
self._display_image(display_img)
|
|
220
|
+
|
|
221
|
+
def _zoom_in(self):
|
|
222
|
+
"""Zoom in by 20%."""
|
|
223
|
+
self.zoom_factor *= 1.2
|
|
224
|
+
self._apply_display()
|
|
225
|
+
|
|
226
|
+
def _zoom_out(self):
|
|
227
|
+
"""Zoom out by 20%."""
|
|
228
|
+
self.zoom_factor /= 1.2
|
|
229
|
+
self._apply_display()
|
|
230
|
+
|
|
231
|
+
def _center_scrollbars(self):
|
|
232
|
+
"""Center the scroll area on the image."""
|
|
233
|
+
h_bar = self.scroll_area.horizontalScrollBar()
|
|
234
|
+
v_bar = self.scroll_area.verticalScrollBar()
|
|
235
|
+
h_bar.setValue((h_bar.maximum() + h_bar.minimum()) // 2)
|
|
236
|
+
v_bar.setValue((v_bar.maximum() + v_bar.minimum()) // 2)
|
|
237
|
+
|
|
238
|
+
def eventFilter(self, source, event):
|
|
239
|
+
# --- wheel zoom (keep exactly as you had) ---
|
|
240
|
+
if source in (self.image_label, self.scroll_area.viewport()) and event.type() == QEvent.Type.Wheel:
|
|
241
|
+
if event.angleDelta().y() > 0:
|
|
242
|
+
self._zoom_in()
|
|
243
|
+
else:
|
|
244
|
+
self._zoom_out()
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
# --- drag-to-pan ---
|
|
248
|
+
if source in (self.image_label, self.scroll_area.viewport()):
|
|
249
|
+
|
|
250
|
+
if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
|
|
251
|
+
self._panning = True
|
|
252
|
+
self._pan_last = event.globalPosition().toPoint()
|
|
253
|
+
# nice UX
|
|
254
|
+
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
if event.type() == QEvent.Type.MouseMove and self._panning:
|
|
258
|
+
cur = event.globalPosition().toPoint()
|
|
259
|
+
delta = cur - self._pan_last
|
|
260
|
+
self._pan_last = cur
|
|
261
|
+
|
|
262
|
+
h = self.scroll_area.horizontalScrollBar()
|
|
263
|
+
v = self.scroll_area.verticalScrollBar()
|
|
264
|
+
h.setValue(h.value() - delta.x())
|
|
265
|
+
v.setValue(v.value() - delta.y())
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
if event.type() == QEvent.Type.MouseButtonRelease and event.button() == Qt.MouseButton.LeftButton and self._panning:
|
|
269
|
+
self._panning = False
|
|
270
|
+
self.unsetCursor()
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
# if mouse leaves while dragging, stop panning so it doesn't get "stuck"
|
|
274
|
+
if event.type() == QEvent.Type.Leave and self._panning:
|
|
275
|
+
self._panning = False
|
|
276
|
+
self.unsetCursor()
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
return super().eventFilter(source, event)
|
|
280
|
+
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# pro/widgets/spinboxes.py
|
|
2
|
+
"""
|
|
3
|
+
Custom spinbox widgets for Seti Astro Suite Pro.
|
|
4
|
+
|
|
5
|
+
Provides enhanced spinbox widgets with consistent styling and behavior.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
|
10
|
+
from PyQt6.QtWidgets import (
|
|
11
|
+
QWidget, QHBoxLayout, QVBoxLayout, QLineEdit, QToolButton
|
|
12
|
+
)
|
|
13
|
+
from PyQt6.QtGui import QIntValidator, QDoubleValidator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CustomSpinBox(QWidget):
|
|
17
|
+
"""
|
|
18
|
+
A custom integer spin box widget with up/down buttons.
|
|
19
|
+
|
|
20
|
+
Emits valueChanged(int) when the value changes.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
spin = CustomSpinBox(minimum=0, maximum=100, initial=50, step=1)
|
|
24
|
+
spin.valueChanged.connect(my_handler)
|
|
25
|
+
"""
|
|
26
|
+
valueChanged = pyqtSignal(int)
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
minimum: int = 0,
|
|
31
|
+
maximum: int = 100,
|
|
32
|
+
initial: int = 0,
|
|
33
|
+
step: int = 1,
|
|
34
|
+
parent: QWidget | None = None
|
|
35
|
+
):
|
|
36
|
+
super().__init__(parent)
|
|
37
|
+
self.minimum = minimum
|
|
38
|
+
self.maximum = maximum
|
|
39
|
+
self.step = step
|
|
40
|
+
self._value = initial
|
|
41
|
+
|
|
42
|
+
# Line edit for value display/entry
|
|
43
|
+
self.lineEdit = QLineEdit(str(initial))
|
|
44
|
+
self.lineEdit.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
45
|
+
self.lineEdit.setValidator(QIntValidator(self.minimum, self.maximum, self))
|
|
46
|
+
self.lineEdit.editingFinished.connect(self._on_editing_finished)
|
|
47
|
+
|
|
48
|
+
# Up/down buttons
|
|
49
|
+
self.upButton = QToolButton()
|
|
50
|
+
self.upButton.setText("▲")
|
|
51
|
+
self.upButton.setAutoRepeat(True)
|
|
52
|
+
self.upButton.setAutoRepeatInterval(50)
|
|
53
|
+
self.upButton.setAutoRepeatDelay(300)
|
|
54
|
+
self.upButton.clicked.connect(self._increase_value)
|
|
55
|
+
|
|
56
|
+
self.downButton = QToolButton()
|
|
57
|
+
self.downButton.setText("▼")
|
|
58
|
+
self.downButton.setAutoRepeat(True)
|
|
59
|
+
self.downButton.setAutoRepeatInterval(50)
|
|
60
|
+
self.downButton.setAutoRepeatDelay(300)
|
|
61
|
+
self.downButton.clicked.connect(self._decrease_value)
|
|
62
|
+
|
|
63
|
+
# Layout buttons vertically
|
|
64
|
+
button_layout = QVBoxLayout()
|
|
65
|
+
button_layout.addWidget(self.upButton)
|
|
66
|
+
button_layout.addWidget(self.downButton)
|
|
67
|
+
button_layout.setSpacing(0)
|
|
68
|
+
button_layout.setContentsMargins(0, 0, 0, 0)
|
|
69
|
+
|
|
70
|
+
# Main horizontal layout
|
|
71
|
+
main_layout = QHBoxLayout()
|
|
72
|
+
main_layout.addWidget(self.lineEdit)
|
|
73
|
+
main_layout.addLayout(button_layout)
|
|
74
|
+
main_layout.setSpacing(0)
|
|
75
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
76
|
+
self.setLayout(main_layout)
|
|
77
|
+
|
|
78
|
+
self._update_button_states()
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def value(self) -> int:
|
|
82
|
+
"""Get the current value."""
|
|
83
|
+
return self._value
|
|
84
|
+
|
|
85
|
+
def setValue(self, val: int) -> None:
|
|
86
|
+
"""Set the value, clamping to min/max."""
|
|
87
|
+
val = max(self.minimum, min(self.maximum, val))
|
|
88
|
+
if val != self._value:
|
|
89
|
+
self._value = val
|
|
90
|
+
self.lineEdit.setText(str(val))
|
|
91
|
+
self.valueChanged.emit(val)
|
|
92
|
+
self._update_button_states()
|
|
93
|
+
|
|
94
|
+
def setMinimum(self, minimum: int) -> None:
|
|
95
|
+
"""Set the minimum value."""
|
|
96
|
+
self.minimum = minimum
|
|
97
|
+
self.lineEdit.setValidator(QIntValidator(self.minimum, self.maximum, self))
|
|
98
|
+
if self._value < minimum:
|
|
99
|
+
self.setValue(minimum)
|
|
100
|
+
self._update_button_states()
|
|
101
|
+
|
|
102
|
+
def setMaximum(self, maximum: int) -> None:
|
|
103
|
+
"""Set the maximum value."""
|
|
104
|
+
self.maximum = maximum
|
|
105
|
+
self.lineEdit.setValidator(QIntValidator(self.minimum, self.maximum, self))
|
|
106
|
+
if self._value > maximum:
|
|
107
|
+
self.setValue(maximum)
|
|
108
|
+
self._update_button_states()
|
|
109
|
+
|
|
110
|
+
def setRange(self, minimum: int, maximum: int) -> None:
|
|
111
|
+
"""Set both minimum and maximum values."""
|
|
112
|
+
self.minimum = minimum
|
|
113
|
+
self.maximum = maximum
|
|
114
|
+
self.lineEdit.setValidator(QIntValidator(self.minimum, self.maximum, self))
|
|
115
|
+
self.setValue(max(minimum, min(maximum, self._value)))
|
|
116
|
+
|
|
117
|
+
def setSingleStep(self, step: int) -> None:
|
|
118
|
+
"""Set the step value for up/down buttons."""
|
|
119
|
+
self.step = step
|
|
120
|
+
|
|
121
|
+
def _on_editing_finished(self) -> None:
|
|
122
|
+
"""Handle manual text entry."""
|
|
123
|
+
try:
|
|
124
|
+
val = int(self.lineEdit.text())
|
|
125
|
+
self.setValue(val)
|
|
126
|
+
except ValueError:
|
|
127
|
+
self.lineEdit.setText(str(self._value))
|
|
128
|
+
|
|
129
|
+
def _increase_value(self) -> None:
|
|
130
|
+
"""Increase value by step."""
|
|
131
|
+
self.setValue(self._value + self.step)
|
|
132
|
+
|
|
133
|
+
def _decrease_value(self) -> None:
|
|
134
|
+
"""Decrease value by step."""
|
|
135
|
+
self.setValue(self._value - self.step)
|
|
136
|
+
|
|
137
|
+
def _update_button_states(self) -> None:
|
|
138
|
+
"""Enable/disable buttons at limits."""
|
|
139
|
+
self.upButton.setEnabled(self._value < self.maximum)
|
|
140
|
+
self.downButton.setEnabled(self._value > self.minimum)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class CustomDoubleSpinBox(QWidget):
|
|
144
|
+
"""
|
|
145
|
+
A custom double (float) spin box widget with up/down buttons.
|
|
146
|
+
|
|
147
|
+
Emits valueChanged(float) when the value changes.
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
spin = CustomDoubleSpinBox(minimum=0.0, maximum=1.0, initial=0.5, step=0.1, decimals=2)
|
|
151
|
+
spin.valueChanged.connect(my_handler)
|
|
152
|
+
"""
|
|
153
|
+
valueChanged = pyqtSignal(float)
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
minimum: float = 0.0,
|
|
158
|
+
maximum: float = 100.0,
|
|
159
|
+
initial: float = 0.0,
|
|
160
|
+
step: float = 1.0,
|
|
161
|
+
decimals: int = 2,
|
|
162
|
+
parent: QWidget | None = None
|
|
163
|
+
):
|
|
164
|
+
super().__init__(parent)
|
|
165
|
+
self.minimum = minimum
|
|
166
|
+
self.maximum = maximum
|
|
167
|
+
self.step = step
|
|
168
|
+
self.decimals = decimals
|
|
169
|
+
self._value = initial
|
|
170
|
+
|
|
171
|
+
# Line edit for value display/entry
|
|
172
|
+
self.lineEdit = QLineEdit(f"{initial:.{decimals}f}")
|
|
173
|
+
self.lineEdit.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
174
|
+
self.lineEdit.setValidator(QDoubleValidator(self.minimum, self.maximum, decimals, self))
|
|
175
|
+
self.lineEdit.editingFinished.connect(self._on_editing_finished)
|
|
176
|
+
|
|
177
|
+
# Up/down buttons
|
|
178
|
+
self.upButton = QToolButton()
|
|
179
|
+
self.upButton.setText("▲")
|
|
180
|
+
self.upButton.setAutoRepeat(True)
|
|
181
|
+
self.upButton.setAutoRepeatInterval(50)
|
|
182
|
+
self.upButton.setAutoRepeatDelay(300)
|
|
183
|
+
self.upButton.clicked.connect(self._increase_value)
|
|
184
|
+
|
|
185
|
+
self.downButton = QToolButton()
|
|
186
|
+
self.downButton.setText("▼")
|
|
187
|
+
self.downButton.setAutoRepeat(True)
|
|
188
|
+
self.downButton.setAutoRepeatInterval(50)
|
|
189
|
+
self.downButton.setAutoRepeatDelay(300)
|
|
190
|
+
self.downButton.clicked.connect(self._decrease_value)
|
|
191
|
+
|
|
192
|
+
# Layout buttons vertically
|
|
193
|
+
button_layout = QVBoxLayout()
|
|
194
|
+
button_layout.addWidget(self.upButton)
|
|
195
|
+
button_layout.addWidget(self.downButton)
|
|
196
|
+
button_layout.setSpacing(0)
|
|
197
|
+
button_layout.setContentsMargins(0, 0, 0, 0)
|
|
198
|
+
|
|
199
|
+
# Main horizontal layout
|
|
200
|
+
main_layout = QHBoxLayout()
|
|
201
|
+
main_layout.addWidget(self.lineEdit)
|
|
202
|
+
main_layout.addLayout(button_layout)
|
|
203
|
+
main_layout.setSpacing(0)
|
|
204
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
205
|
+
self.setLayout(main_layout)
|
|
206
|
+
|
|
207
|
+
self._update_button_states()
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def value(self) -> float:
|
|
211
|
+
"""Get the current value."""
|
|
212
|
+
return self._value
|
|
213
|
+
|
|
214
|
+
def setValue(self, val: float) -> None:
|
|
215
|
+
"""Set the value, clamping to min/max."""
|
|
216
|
+
val = max(self.minimum, min(self.maximum, val))
|
|
217
|
+
if abs(val - self._value) > 1e-10:
|
|
218
|
+
self._value = val
|
|
219
|
+
self.lineEdit.setText(f"{val:.{self.decimals}f}")
|
|
220
|
+
self.valueChanged.emit(val)
|
|
221
|
+
self._update_button_states()
|
|
222
|
+
|
|
223
|
+
def setMinimum(self, minimum: float) -> None:
|
|
224
|
+
"""Set the minimum value."""
|
|
225
|
+
self.minimum = minimum
|
|
226
|
+
self.lineEdit.setValidator(QDoubleValidator(self.minimum, self.maximum, self.decimals, self))
|
|
227
|
+
if self._value < minimum:
|
|
228
|
+
self.setValue(minimum)
|
|
229
|
+
self._update_button_states()
|
|
230
|
+
|
|
231
|
+
def setMaximum(self, maximum: float) -> None:
|
|
232
|
+
"""Set the maximum value."""
|
|
233
|
+
self.maximum = maximum
|
|
234
|
+
self.lineEdit.setValidator(QDoubleValidator(self.minimum, self.maximum, self.decimals, self))
|
|
235
|
+
if self._value > maximum:
|
|
236
|
+
self.setValue(maximum)
|
|
237
|
+
self._update_button_states()
|
|
238
|
+
|
|
239
|
+
def setRange(self, minimum: float, maximum: float) -> None:
|
|
240
|
+
"""Set both minimum and maximum values."""
|
|
241
|
+
self.minimum = minimum
|
|
242
|
+
self.maximum = maximum
|
|
243
|
+
self.lineEdit.setValidator(QDoubleValidator(self.minimum, self.maximum, self.decimals, self))
|
|
244
|
+
self.setValue(max(minimum, min(maximum, self._value)))
|
|
245
|
+
|
|
246
|
+
def setSingleStep(self, step: float) -> None:
|
|
247
|
+
"""Set the step value for up/down buttons."""
|
|
248
|
+
self.step = step
|
|
249
|
+
|
|
250
|
+
def setDecimals(self, decimals: int) -> None:
|
|
251
|
+
"""Set the number of decimal places."""
|
|
252
|
+
self.decimals = decimals
|
|
253
|
+
self.lineEdit.setText(f"{self._value:.{decimals}f}")
|
|
254
|
+
self.lineEdit.setValidator(QDoubleValidator(self.minimum, self.maximum, decimals, self))
|
|
255
|
+
|
|
256
|
+
def _on_editing_finished(self) -> None:
|
|
257
|
+
"""Handle manual text entry."""
|
|
258
|
+
try:
|
|
259
|
+
val = float(self.lineEdit.text())
|
|
260
|
+
self.setValue(val)
|
|
261
|
+
except ValueError:
|
|
262
|
+
self.lineEdit.setText(f"{self._value:.{self.decimals}f}")
|
|
263
|
+
|
|
264
|
+
def _increase_value(self) -> None:
|
|
265
|
+
"""Increase value by step."""
|
|
266
|
+
self.setValue(self._value + self.step)
|
|
267
|
+
|
|
268
|
+
def _decrease_value(self) -> None:
|
|
269
|
+
"""Decrease value by step."""
|
|
270
|
+
self.setValue(self._value - self.step)
|
|
271
|
+
|
|
272
|
+
def _update_button_states(self) -> None:
|
|
273
|
+
"""Enable/disable buttons at limits."""
|
|
274
|
+
self.upButton.setEnabled(self._value < self.maximum - 1e-10)
|
|
275
|
+
self.downButton.setEnabled(self._value > self.minimum + 1e-10)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# pro/widgets/themed_buttons.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from PyQt6.QtGui import QIcon
|
|
4
|
+
from PyQt6.QtWidgets import QToolButton
|
|
5
|
+
|
|
6
|
+
def themed_toolbtn(icon_name: str, tip: str, *, on_click=None) -> QToolButton:
|
|
7
|
+
b = QToolButton()
|
|
8
|
+
b.setIcon(QIcon.fromTheme(icon_name))
|
|
9
|
+
b.setToolTip(tip)
|
|
10
|
+
b.setAutoRaise(True) # nice flat toolbar look
|
|
11
|
+
if on_click is not None:
|
|
12
|
+
b.clicked.connect(on_click)
|
|
13
|
+
return b
|