setiastrosuitepro 1.6.7__py3-none-any.whl → 1.6.10__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.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/add_stars.py +29 -5
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/cosmicclarity.py +125 -18
- setiastro/saspro/crop_dialog_pro.py +96 -2
- setiastro/saspro/curve_editor_pro.py +60 -39
- setiastro/saspro/frequency_separation.py +1159 -208
- setiastro/saspro/gui/main_window.py +131 -31
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/update_mixin.py +121 -33
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/live_stacking.py +158 -70
- setiastro/saspro/multiscale_decomp.py +47 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/shortcuts.py +122 -12
- setiastro/saspro/signature_insert.py +688 -33
- setiastro/saspro/stacking_suite.py +523 -316
- setiastro/saspro/stat_stretch.py +688 -130
- setiastro/saspro/subwindow.py +302 -71
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +7 -7
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +37 -31
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
|
@@ -361,6 +361,69 @@ class UiStallDetector(QObject):
|
|
|
361
361
|
print(f"[UI STALL] tick late by {late_ms:.0f} ms (elapsed={elapsed_ms:.0f} ms)", flush=True)
|
|
362
362
|
self._dump_all_threads_print()
|
|
363
363
|
|
|
364
|
+
def _strip_filename_ext(title: str) -> str:
|
|
365
|
+
t = (title or "").strip()
|
|
366
|
+
if not t:
|
|
367
|
+
return t
|
|
368
|
+
base, ext = os.path.splitext(t)
|
|
369
|
+
# treat as extension only if it looks like one: .fit .fits .tif .tiff .xisf etc
|
|
370
|
+
if ext and 1 <= len(ext) <= 10 and all(ch.isalnum() for ch in ext[1:]):
|
|
371
|
+
return base
|
|
372
|
+
return t
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
_DECOR_GLYPHS = "■●◆▲▪▫•◼◻◾◽🔗"
|
|
377
|
+
|
|
378
|
+
def normalize_doc_title(s: str) -> str:
|
|
379
|
+
s = (s or "").strip()
|
|
380
|
+
|
|
381
|
+
# remove our textual prefix too
|
|
382
|
+
if s.startswith("[LINK] "):
|
|
383
|
+
s = s[len("[LINK] "):].strip()
|
|
384
|
+
|
|
385
|
+
# strip common UI decorations if you already have this helper
|
|
386
|
+
try:
|
|
387
|
+
s = _strip_ui_decorations(s)
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
# remove any leading decorator glyphs repeatedly: "🔗 ", "■ ", etc.
|
|
392
|
+
while len(s) >= 2 and s[0] in _DECOR_GLYPHS and s[1] == " ":
|
|
393
|
+
s = s[2:].lstrip()
|
|
394
|
+
|
|
395
|
+
# also remove any stray decorator glyphs that got embedded (rare but happens)
|
|
396
|
+
s = re.sub(rf"[{re.escape(_DECOR_GLYPHS)}]", "", s).strip()
|
|
397
|
+
|
|
398
|
+
return s
|
|
399
|
+
|
|
400
|
+
_VIEW_SUFFIX_RE = re.compile(r"\s+\[View\s+\d+\]\s*$")
|
|
401
|
+
|
|
402
|
+
def _normalize_title_for_compare(t: str) -> str:
|
|
403
|
+
t = (t or "").strip()
|
|
404
|
+
if not t:
|
|
405
|
+
return ""
|
|
406
|
+
|
|
407
|
+
# strip UI decorations (🔗, ■, etc)
|
|
408
|
+
try:
|
|
409
|
+
t = _strip_ui_decorations(t)
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
# strip trailing "[View N]" if present
|
|
414
|
+
t = _VIEW_SUFFIX_RE.sub("", t).strip()
|
|
415
|
+
|
|
416
|
+
# strip filename-like extension
|
|
417
|
+
try:
|
|
418
|
+
t = _strip_filename_ext(t)
|
|
419
|
+
except Exception:
|
|
420
|
+
# fallback: only strip if it looks like an ext
|
|
421
|
+
base, ext = os.path.splitext(t)
|
|
422
|
+
if ext and len(ext) <= 10:
|
|
423
|
+
t = base
|
|
424
|
+
|
|
425
|
+
return t.strip()
|
|
426
|
+
|
|
364
427
|
class AstroSuiteProMainWindow(
|
|
365
428
|
DockMixin, MenuMixin, ToolbarMixin, FileMixin,
|
|
366
429
|
ThemeMixin, GeometryMixin, ViewMixin, HeaderMixin, MaskMixin, UpdateMixin,
|
|
@@ -7779,12 +7842,8 @@ class AstroSuiteProMainWindow(
|
|
|
7779
7842
|
return cand
|
|
7780
7843
|
n += 1
|
|
7781
7844
|
|
|
7845
|
+
|
|
7782
7846
|
def _doc_window_title(self, doc) -> str:
|
|
7783
|
-
"""
|
|
7784
|
-
Best-effort human title for a subwindow.
|
|
7785
|
-
Prefer metadata['display_name'] (what duplication sets),
|
|
7786
|
-
then doc.display_name(), then basename(file_path).
|
|
7787
|
-
"""
|
|
7788
7847
|
md = getattr(doc, "metadata", {}) or {}
|
|
7789
7848
|
|
|
7790
7849
|
t = (md.get("display_name") or "").strip()
|
|
@@ -7797,8 +7856,7 @@ class AstroSuiteProMainWindow(
|
|
|
7797
7856
|
if not t:
|
|
7798
7857
|
fp = (md.get("file_path") or "").strip()
|
|
7799
7858
|
if fp:
|
|
7800
|
-
|
|
7801
|
-
t = os.path.basename(fp)
|
|
7859
|
+
t = os.path.splitext(os.path.basename(fp))[0] # ✅ strip ext here too
|
|
7802
7860
|
|
|
7803
7861
|
t = t or "Untitled"
|
|
7804
7862
|
|
|
@@ -7807,8 +7865,11 @@ class AstroSuiteProMainWindow(
|
|
|
7807
7865
|
t = _strip_ui_decorations(t)
|
|
7808
7866
|
except Exception:
|
|
7809
7867
|
pass
|
|
7810
|
-
return t
|
|
7811
7868
|
|
|
7869
|
+
# ✅ ALWAYS strip filename-like extension at the very end
|
|
7870
|
+
t = _strip_filename_ext(t)
|
|
7871
|
+
|
|
7872
|
+
return t
|
|
7812
7873
|
|
|
7813
7874
|
def _spawn_subwindow_for(self, doc, *, force_new: bool = False):
|
|
7814
7875
|
"""
|
|
@@ -7945,7 +8006,8 @@ class AstroSuiteProMainWindow(
|
|
|
7945
8006
|
# We target ~60% of the viewport height, clamped to sane bounds.
|
|
7946
8007
|
# -------------------------------------------------------------------------
|
|
7947
8008
|
vp = self.mdi.viewport()
|
|
7948
|
-
|
|
8009
|
+
# Use viewport geometry in MDI coordinates (NOT viewport-local rect)
|
|
8010
|
+
area = vp.geometry() if vp else self.mdi.contentsRect()
|
|
7949
8011
|
|
|
7950
8012
|
# Determine aspect ratio
|
|
7951
8013
|
img_w = img_h = None
|
|
@@ -7988,7 +8050,7 @@ class AstroSuiteProMainWindow(
|
|
|
7988
8050
|
# Smart Cascade: Position relative to the *currently active* window
|
|
7989
8051
|
# (before we make the new one active).
|
|
7990
8052
|
# -------------------------------------------------------------------------
|
|
7991
|
-
new_x, new_y =
|
|
8053
|
+
new_x, new_y = area.left(), area.top()
|
|
7992
8054
|
|
|
7993
8055
|
# Get dominant/active window *before* we activate the new one
|
|
7994
8056
|
active = self.mdi.activeSubWindow()
|
|
@@ -8011,15 +8073,13 @@ class AstroSuiteProMainWindow(
|
|
|
8011
8073
|
except Exception:
|
|
8012
8074
|
pass
|
|
8013
8075
|
|
|
8014
|
-
# Bounds check:
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
new_x = max(0, new_x)
|
|
8022
|
-
new_y = max(0, new_y)
|
|
8076
|
+
# Bounds check: keep titlebar visible and stay inside viewport
|
|
8077
|
+
if (new_x + target_w > area.right() - 10) or (new_y + 40 > area.bottom() - 10):
|
|
8078
|
+
new_x = area.left()
|
|
8079
|
+
new_y = area.top()
|
|
8080
|
+
|
|
8081
|
+
new_x = max(area.left(), new_x)
|
|
8082
|
+
new_y = max(area.top(), new_y)
|
|
8023
8083
|
|
|
8024
8084
|
sw.move(new_x, new_y)
|
|
8025
8085
|
|
|
@@ -8114,6 +8174,11 @@ class AstroSuiteProMainWindow(
|
|
|
8114
8174
|
except Exception:
|
|
8115
8175
|
pass
|
|
8116
8176
|
|
|
8177
|
+
try:
|
|
8178
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8179
|
+
except Exception:
|
|
8180
|
+
pass
|
|
8181
|
+
|
|
8117
8182
|
# -- 11) If this is the first window and it's an image, mimic "Cascade Views"
|
|
8118
8183
|
try:
|
|
8119
8184
|
if first_window and not is_table:
|
|
@@ -8208,25 +8273,57 @@ class AstroSuiteProMainWindow(
|
|
|
8208
8273
|
"autostretch_target": float(getattr(source_view, "autostretch_target", 0.25)),
|
|
8209
8274
|
}
|
|
8210
8275
|
|
|
8211
|
-
# 2) New name (
|
|
8212
|
-
base_name = ""
|
|
8276
|
+
# 2) New name (normalized: NO decorators like 🔗■●◆▲▪▫•◼◻◾◽)
|
|
8213
8277
|
try:
|
|
8214
|
-
base_name =
|
|
8278
|
+
base_name = self._doc_window_title(base_doc) # might include decorations
|
|
8215
8279
|
except Exception:
|
|
8216
8280
|
base_name = "Untitled"
|
|
8217
8281
|
|
|
8282
|
+
# Normalize it so uniqueness checks don't miss decorated titles
|
|
8283
|
+
try:
|
|
8284
|
+
base_name = normalize_doc_title(base_name)
|
|
8285
|
+
except Exception:
|
|
8286
|
+
base_name = (base_name or "Untitled").strip()
|
|
8287
|
+
|
|
8288
|
+
# Build a set of existing document names (normalized)
|
|
8289
|
+
existing = set()
|
|
8218
8290
|
try:
|
|
8219
|
-
|
|
8291
|
+
dm = getattr(self, "doc_manager", None) or getattr(self, "docman", None)
|
|
8292
|
+
docs = []
|
|
8293
|
+
|
|
8294
|
+
# Prefer an official accessor if you have one
|
|
8295
|
+
if dm is not None:
|
|
8296
|
+
if hasattr(dm, "documents"):
|
|
8297
|
+
docs = list(dm.documents())
|
|
8298
|
+
elif hasattr(dm, "_docs"):
|
|
8299
|
+
docs = list(dm._docs)
|
|
8300
|
+
|
|
8301
|
+
for d in docs:
|
|
8302
|
+
try:
|
|
8303
|
+
dn = ""
|
|
8304
|
+
md = getattr(d, "metadata", {}) or {}
|
|
8305
|
+
dn = (md.get("display_name") or "").strip() or (d.display_name() or "").strip()
|
|
8306
|
+
dn = normalize_doc_title(dn)
|
|
8307
|
+
if dn:
|
|
8308
|
+
existing.add(dn)
|
|
8309
|
+
except Exception:
|
|
8310
|
+
pass
|
|
8220
8311
|
except Exception:
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8312
|
+
pass
|
|
8313
|
+
|
|
8314
|
+
# Pick a unique duplicate name: base_duplicate, base_duplicate2, ...
|
|
8315
|
+
candidate = f"{base_name}_duplicate"
|
|
8316
|
+
if candidate in existing:
|
|
8317
|
+
n = 2
|
|
8318
|
+
while True:
|
|
8319
|
+
cand = f"{base_name}_duplicate{n}"
|
|
8320
|
+
if cand not in existing:
|
|
8321
|
+
candidate = cand
|
|
8322
|
+
break
|
|
8323
|
+
n += 1
|
|
8226
8324
|
|
|
8227
8325
|
# 3) Duplicate the *base* document (not the ROI proxy)
|
|
8228
|
-
|
|
8229
|
-
new_doc = self.docman.duplicate_document(base_doc, new_name=f"{base_name}_duplicate")
|
|
8326
|
+
new_doc = self.docman.duplicate_document(base_doc, new_name=candidate)
|
|
8230
8327
|
print(f" Duplicated document ID {id(base_doc)} -> {id(new_doc)}")
|
|
8231
8328
|
|
|
8232
8329
|
# 4) Ensure the duplicate starts mask-free (so we don't inherit mask UI state)
|
|
@@ -8521,7 +8618,10 @@ class AstroSuiteProMainWindow(
|
|
|
8521
8618
|
self._refresh_mask_action_states()
|
|
8522
8619
|
except Exception:
|
|
8523
8620
|
pass
|
|
8524
|
-
|
|
8621
|
+
try:
|
|
8622
|
+
self._fix_mdi_titlebar_emboss("#dcdcdc" if self._theme_mode()=="dark" else "#f0f0f0")
|
|
8623
|
+
except Exception:
|
|
8624
|
+
pass
|
|
8525
8625
|
|
|
8526
8626
|
def _sync_docman_active(self, doc):
|
|
8527
8627
|
dm = self.doc_manager
|
|
@@ -9,12 +9,30 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from PyQt6.QtCore import Qt, QTimer
|
|
12
|
-
from PyQt6.QtGui import QBrush, QColor, QFont, QPalette
|
|
13
|
-
from PyQt6.QtWidgets import QApplication
|
|
12
|
+
from PyQt6.QtGui import QBrush, QColor, QFont, QPalette, QPainter, QPixmap, QIcon
|
|
13
|
+
from PyQt6.QtWidgets import QApplication, QLabel, QWidget
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
pass
|
|
17
17
|
|
|
18
|
+
def _force_mdi_subwindow_flags(sw):
|
|
19
|
+
f = sw.windowFlags()
|
|
20
|
+
|
|
21
|
+
# Clear only the *window type* bits (NOT random low bits)
|
|
22
|
+
f &= ~Qt.WindowType.WindowType_Mask
|
|
23
|
+
|
|
24
|
+
# Force true MDI child type
|
|
25
|
+
f |= Qt.WindowType.SubWindow
|
|
26
|
+
|
|
27
|
+
# Add desired buttons/hints
|
|
28
|
+
f |= (Qt.WindowType.CustomizeWindowHint |
|
|
29
|
+
Qt.WindowType.WindowTitleHint |
|
|
30
|
+
Qt.WindowType.WindowSystemMenuHint |
|
|
31
|
+
Qt.WindowType.WindowMinimizeButtonHint |
|
|
32
|
+
Qt.WindowType.WindowMaximizeButtonHint |
|
|
33
|
+
Qt.WindowType.WindowCloseButtonHint)
|
|
34
|
+
|
|
35
|
+
sw.setWindowFlags(f)
|
|
18
36
|
|
|
19
37
|
class ThemeMixin:
|
|
20
38
|
"""
|
|
@@ -88,7 +106,7 @@ class ThemeMixin:
|
|
|
88
106
|
app.setPalette(self._gray_palette())
|
|
89
107
|
app.setStyleSheet(
|
|
90
108
|
"QToolTip { color: #f0f0f0; background-color: #3a3a3a; border: 1px solid #5a5a5a; }"
|
|
91
|
-
)
|
|
109
|
+
)
|
|
92
110
|
elif mode == "light":
|
|
93
111
|
app.setPalette(self._light_palette())
|
|
94
112
|
app.setStyleSheet(
|
|
@@ -119,6 +137,11 @@ class ThemeMixin:
|
|
|
119
137
|
self._repolish_top_levels()
|
|
120
138
|
self._apply_workspace_theme()
|
|
121
139
|
self._style_mdi_titlebars()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
self._retint_zoom_icons()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
122
145
|
self._menu_view_panels = None
|
|
123
146
|
|
|
124
147
|
try:
|
|
@@ -139,26 +162,149 @@ class ThemeMixin:
|
|
|
139
162
|
w.setUpdatesEnabled(True)
|
|
140
163
|
|
|
141
164
|
def _style_mdi_titlebars(self):
|
|
142
|
-
"""Apply theme-specific styles to MDI subwindow titlebars."""
|
|
143
165
|
mode = self._theme_mode()
|
|
166
|
+
|
|
144
167
|
if mode == "dark":
|
|
145
|
-
base = "#1b1b1b"
|
|
146
|
-
active = "#242424"
|
|
168
|
+
base = "#1b1b1b"
|
|
169
|
+
active = "#242424"
|
|
147
170
|
fg = "#dcdcdc"
|
|
148
171
|
elif mode in ("gray", "custom"):
|
|
149
172
|
base = "#3a3a3a"
|
|
150
173
|
active = "#454545"
|
|
151
174
|
fg = "#f0f0f0"
|
|
152
175
|
else:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
base = "#eaeaea"
|
|
177
|
+
active = "#ffffff"
|
|
178
|
+
fg = "#141414"
|
|
156
179
|
|
|
180
|
+
# style *our* titlebar only
|
|
157
181
|
self.mdi.setStyleSheet(f"""
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
QWidget#sas_mdi_titlebar {{
|
|
183
|
+
background: {base};
|
|
184
|
+
}}
|
|
185
|
+
QWidget#sas_mdi_titlebar[active="true"] {{
|
|
186
|
+
background: {active};
|
|
187
|
+
}}
|
|
188
|
+
QLabel#sas_mdi_title_label {{
|
|
189
|
+
color: {fg};
|
|
190
|
+
background: transparent;
|
|
191
|
+
}}
|
|
192
|
+
QWidget#sas_mdi_titlebar QToolButton {{
|
|
193
|
+
color: {fg};
|
|
194
|
+
background: transparent;
|
|
195
|
+
}}
|
|
196
|
+
QWidget#sas_mdi_titlebar QToolButton:hover {{
|
|
197
|
+
background: rgba(255,255,255,0.10);
|
|
198
|
+
}}
|
|
160
199
|
""")
|
|
161
200
|
|
|
201
|
+
|
|
202
|
+
def _fix_mdi_titlebar_emboss(self, fg_hex: str):
|
|
203
|
+
"""
|
|
204
|
+
Fusion/Windows style can draw embossed title text (two-pass).
|
|
205
|
+
In dark themes that can become white-on-white -> 'double text'.
|
|
206
|
+
Force the shadow/emboss colors on the *titlebar widget only*.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
fg = QColor(fg_hex)
|
|
210
|
+
except Exception:
|
|
211
|
+
fg = QColor(240, 240, 240)
|
|
212
|
+
|
|
213
|
+
for sw in self.mdi.subWindowList():
|
|
214
|
+
try:
|
|
215
|
+
tb = sw.findChild(QWidget, "qt_mdi_titlebar")
|
|
216
|
+
if tb is None:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
pal = tb.palette()
|
|
220
|
+
|
|
221
|
+
# Main text
|
|
222
|
+
pal.setColor(QPalette.ColorRole.WindowText, fg)
|
|
223
|
+
pal.setColor(QPalette.ColorRole.Text, fg)
|
|
224
|
+
pal.setColor(QPalette.ColorRole.ButtonText, fg)
|
|
225
|
+
|
|
226
|
+
# Critical: make the embossed/shadow pass dark
|
|
227
|
+
dark = QColor(0, 0, 0)
|
|
228
|
+
pal.setColor(QPalette.ColorRole.Light, dark)
|
|
229
|
+
pal.setColor(QPalette.ColorRole.Midlight, dark)
|
|
230
|
+
pal.setColor(QPalette.ColorRole.Dark, dark)
|
|
231
|
+
pal.setColor(QPalette.ColorRole.Shadow, dark)
|
|
232
|
+
|
|
233
|
+
tb.setPalette(pal)
|
|
234
|
+
|
|
235
|
+
# Also push to the label if present (some styles read it from label)
|
|
236
|
+
lbl = tb.findChild(QLabel)
|
|
237
|
+
if lbl is not None:
|
|
238
|
+
lbl.setPalette(pal)
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
def _tint_icon(self, icon: QIcon, color: QColor) -> QIcon:
|
|
243
|
+
"""
|
|
244
|
+
Take an existing icon (often fromTheme) and force a single-color glyph.
|
|
245
|
+
Sets Normal and Active to the same tinted pixmaps to prevent hover flipping.
|
|
246
|
+
"""
|
|
247
|
+
if icon.isNull():
|
|
248
|
+
return icon
|
|
249
|
+
|
|
250
|
+
out = QIcon()
|
|
251
|
+
sizes = [16, 20, 24, 32, 48, 64]
|
|
252
|
+
|
|
253
|
+
for sz in sizes:
|
|
254
|
+
pm = icon.pixmap(sz, sz, QIcon.Mode.Normal, QIcon.State.Off)
|
|
255
|
+
if pm.isNull():
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
tinted = QPixmap(pm.size())
|
|
259
|
+
tinted.fill(Qt.GlobalColor.transparent)
|
|
260
|
+
|
|
261
|
+
p = QPainter(tinted)
|
|
262
|
+
p.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
263
|
+
p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
|
|
264
|
+
|
|
265
|
+
# Use the original alpha as a mask, fill with our color
|
|
266
|
+
p.drawPixmap(0, 0, pm)
|
|
267
|
+
p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
|
|
268
|
+
p.fillRect(tinted.rect(), color)
|
|
269
|
+
p.end()
|
|
270
|
+
|
|
271
|
+
# Normal + Active -> same pixmap (prevents hover flip)
|
|
272
|
+
out.addPixmap(tinted, QIcon.Mode.Normal, QIcon.State.Off)
|
|
273
|
+
out.addPixmap(tinted, QIcon.Mode.Active, QIcon.State.Off)
|
|
274
|
+
|
|
275
|
+
# Disabled: slightly dimmer (optional)
|
|
276
|
+
dis = QColor(color)
|
|
277
|
+
dis.setAlphaF(0.45)
|
|
278
|
+
dispm = QPixmap(tinted.size())
|
|
279
|
+
dispm.fill(Qt.GlobalColor.transparent)
|
|
280
|
+
p2 = QPainter(dispm)
|
|
281
|
+
p2.drawPixmap(0, 0, tinted)
|
|
282
|
+
p2.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
|
|
283
|
+
p2.fillRect(dispm.rect(), dis)
|
|
284
|
+
p2.end()
|
|
285
|
+
out.addPixmap(dispm, QIcon.Mode.Disabled, QIcon.State.Off)
|
|
286
|
+
|
|
287
|
+
return out
|
|
288
|
+
|
|
289
|
+
def _retint_zoom_icons(self):
|
|
290
|
+
"""
|
|
291
|
+
Retint only the zoom actions (the ones built from QIcon.fromTheme).
|
|
292
|
+
Call after app palette is applied.
|
|
293
|
+
"""
|
|
294
|
+
pal = QApplication.palette()
|
|
295
|
+
glyph = pal.color(QPalette.ColorRole.ButtonText) # or Text; ButtonText tends to match toolbars well
|
|
296
|
+
|
|
297
|
+
for name in ("act_zoom_out", "act_zoom_in", "act_zoom_1_1", "act_zoom_fit"):
|
|
298
|
+
act = getattr(self, name, None)
|
|
299
|
+
if act is None:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# stash original once so repeated theme flips don't re-tint a tinted icon
|
|
303
|
+
if not hasattr(act, "_base_icon"):
|
|
304
|
+
act._base_icon = act.icon()
|
|
305
|
+
|
|
306
|
+
act.setIcon(self._tint_icon(act._base_icon, glyph))
|
|
307
|
+
|
|
162
308
|
def _dark_palette(self) -> QPalette:
|
|
163
309
|
"""Create a dark theme palette."""
|
|
164
310
|
p = QPalette()
|
|
@@ -172,7 +318,7 @@ class ThemeMixin:
|
|
|
172
318
|
hi = QColor(30, 144, 255) # highlight (dodger blue)
|
|
173
319
|
|
|
174
320
|
p.setColor(QPalette.ColorRole.Window, panel)
|
|
175
|
-
p.setColor(QPalette.ColorRole.WindowText,
|
|
321
|
+
p.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
|
|
176
322
|
p.setColor(QPalette.ColorRole.Base, bg)
|
|
177
323
|
p.setColor(QPalette.ColorRole.AlternateBase, altbase)
|
|
178
324
|
p.setColor(QPalette.ColorRole.ToolTipBase, panel)
|
|
@@ -270,7 +416,7 @@ class ThemeMixin:
|
|
|
270
416
|
link = QColor(120, 170, 255)
|
|
271
417
|
linkv = QColor(180, 150, 255)
|
|
272
418
|
hi = QColor(95, 145, 230)
|
|
273
|
-
hitxt = QColor(
|
|
419
|
+
hitxt = QColor(20, 20, 20)
|
|
274
420
|
|
|
275
421
|
# Core roles
|
|
276
422
|
p.setColor(QPalette.ColorRole.Window, window)
|
|
@@ -300,7 +446,7 @@ class ThemeMixin:
|
|
|
300
446
|
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, dis)
|
|
301
447
|
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, QColor(58, 58, 58))
|
|
302
448
|
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Highlight, QColor(80, 80, 80))
|
|
303
|
-
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, QColor(
|
|
449
|
+
p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, QColor(20, 20, 20))
|
|
304
450
|
|
|
305
451
|
return p
|
|
306
452
|
|