triplot 1.1.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.
- dscpanel/__init__.py +4 -0
- dscpanel/__main__.py +155 -0
- dscpanel/branding.py +142 -0
- dscpanel/core/__init__.py +0 -0
- dscpanel/core/arrange.py +143 -0
- dscpanel/core/chem.py +252 -0
- dscpanel/core/dtg.py +135 -0
- dscpanel/core/export.py +649 -0
- dscpanel/core/figure.py +171 -0
- dscpanel/core/labels.py +430 -0
- dscpanel/core/loader.py +202 -0
- dscpanel/core/log.py +147 -0
- dscpanel/core/measure.py +636 -0
- dscpanel/core/model.py +1993 -0
- dscpanel/core/molar.py +344 -0
- dscpanel/core/numbers.py +208 -0
- dscpanel/core/ops.py +161 -0
- dscpanel/core/presets.py +312 -0
- dscpanel/core/profile.py +15 -0
- dscpanel/core/session.py +667 -0
- dscpanel/core/shades.py +54 -0
- dscpanel/core/style.py +528 -0
- dscpanel/core/trios_analysis.py +636 -0
- dscpanel/core/trios_io.py +1311 -0
- dscpanel/core/undo.py +230 -0
- dscpanel/core/units.py +220 -0
- dscpanel/register.py +284 -0
- dscpanel/ui/__init__.py +0 -0
- dscpanel/ui/appearance.py +146 -0
- dscpanel/ui/colour.py +629 -0
- dscpanel/ui/dialogs.py +3639 -0
- dscpanel/ui/loading.py +95 -0
- dscpanel/ui/numbox.py +103 -0
- dscpanel/ui/outliner.py +818 -0
- dscpanel/ui/palette.py +193 -0
- dscpanel/ui/plot.py +8349 -0
- dscpanel/ui/settings.py +256 -0
- dscpanel/ui/window.py +4129 -0
- triplot-1.1.0.dist-info/METADATA +315 -0
- triplot-1.1.0.dist-info/RECORD +44 -0
- triplot-1.1.0.dist-info/WHEEL +5 -0
- triplot-1.1.0.dist-info/entry_points.txt +5 -0
- triplot-1.1.0.dist-info/licenses/LICENSE +22 -0
- triplot-1.1.0.dist-info/top_level.txt +1 -0
dscpanel/register.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Putting the program in the Start Menu, and taking it out again.
|
|
2
|
+
|
|
3
|
+
Two rules:
|
|
4
|
+
|
|
5
|
+
1. **It is opt-in and you run it yourself.** A program that quietly claims a
|
|
6
|
+
file type or writes a shortcut the first time it runs is a program people
|
|
7
|
+
learn to distrust. So nothing here happens at start-up; `triplot
|
|
8
|
+
register` does it and `triplot register --remove` undoes it.
|
|
9
|
+
2. **It writes a manifest of everything it created.** Removal then takes away
|
|
10
|
+
exactly what was added, rather than guessing at paths a later version
|
|
11
|
+
might get wrong - and that manifest, together with
|
|
12
|
+
`branding.LEGACY_NAMES`, is the whole answer to "we will probably rename
|
|
13
|
+
this later": the new name knows what the old one installed and can clean
|
|
14
|
+
it up with `--clean-legacy`.
|
|
15
|
+
|
|
16
|
+
Everything is per user. Nothing here needs administrator rights, nothing is
|
|
17
|
+
written outside the user's own profile, and no registry key outside
|
|
18
|
+
`HKCU\\Software\\Classes`.
|
|
19
|
+
|
|
20
|
+
Aliases are the same machinery pointed at a name the user picks:
|
|
21
|
+
`triplot alias tp` makes `tp` work in a terminal on any platform - a
|
|
22
|
+
`.cmd` shim beside the installed command on Windows, a symlink in
|
|
23
|
+
`~/.local/bin` elsewhere - and records it so `--remove` can take it away.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
from . import branding
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load():
|
|
36
|
+
try:
|
|
37
|
+
with open(branding.manifest_path(), "r", encoding="utf-8") as fh:
|
|
38
|
+
return json.load(fh)
|
|
39
|
+
except (OSError, ValueError):
|
|
40
|
+
return {"entries": []}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _save(manifest):
|
|
44
|
+
os.makedirs(branding.app_dir(), exist_ok=True)
|
|
45
|
+
with open(branding.manifest_path(), "w", encoding="utf-8") as fh:
|
|
46
|
+
json.dump(manifest, fh, indent=1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _record(manifest, kind, path, name=""):
|
|
50
|
+
entry = {"kind": kind, "path": str(path), "name": name,
|
|
51
|
+
"app": branding.APP_NAME}
|
|
52
|
+
if entry not in manifest["entries"]:
|
|
53
|
+
manifest["entries"].append(entry)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def launcher():
|
|
57
|
+
"""The command a shortcut should run: the installed exe if there is one.
|
|
58
|
+
|
|
59
|
+
The windowed entry point is preferred, so no console window appears
|
|
60
|
+
behind the program. Falling back to `python -m dscpanel` keeps this
|
|
61
|
+
working from a source checkout that was never pip-installed.
|
|
62
|
+
"""
|
|
63
|
+
for name in (branding.EXE_NAME_GUI, branding.EXE_NAME):
|
|
64
|
+
found = shutil.which(name)
|
|
65
|
+
if found:
|
|
66
|
+
return [found]
|
|
67
|
+
python = sys.executable
|
|
68
|
+
if sys.platform == "win32":
|
|
69
|
+
windowed = os.path.join(os.path.dirname(python), "pythonw.exe")
|
|
70
|
+
if os.path.isfile(windowed):
|
|
71
|
+
python = windowed
|
|
72
|
+
return [python, "-m", "dscpanel"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------- Windows
|
|
76
|
+
def _windows_start_menu():
|
|
77
|
+
base = os.environ.get("APPDATA") or os.path.expanduser("~")
|
|
78
|
+
return os.path.join(base, "Microsoft", "Windows", "Start Menu",
|
|
79
|
+
"Programs")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _make_shortcut(link_path, command, description):
|
|
83
|
+
"""Write a .lnk through PowerShell's WScript.Shell.
|
|
84
|
+
|
|
85
|
+
Via PowerShell rather than pywin32, so the program does not need a
|
|
86
|
+
dependency whose only job is to make one file.
|
|
87
|
+
"""
|
|
88
|
+
target = command[0]
|
|
89
|
+
arguments = " ".join('"{}"'.format(a) for a in command[1:])
|
|
90
|
+
script = (
|
|
91
|
+
"$s = New-Object -ComObject WScript.Shell; "
|
|
92
|
+
"$l = $s.CreateShortcut('{link}'); "
|
|
93
|
+
"$l.TargetPath = '{target}'; "
|
|
94
|
+
"$l.Arguments = '{args}'; "
|
|
95
|
+
"$l.Description = '{desc}'; "
|
|
96
|
+
"$l.WorkingDirectory = '{cwd}'; "
|
|
97
|
+
"$l.Save()").format(
|
|
98
|
+
link=link_path.replace("'", "''"),
|
|
99
|
+
target=target.replace("'", "''"),
|
|
100
|
+
args=arguments.replace("'", "''"),
|
|
101
|
+
desc=description.replace("'", "''"),
|
|
102
|
+
cwd=os.path.expanduser("~").replace("'", "''"))
|
|
103
|
+
subprocess.run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
|
|
104
|
+
"-Command", script], check=True,
|
|
105
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
106
|
+
return link_path
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _windows_scripts_dir():
|
|
110
|
+
"""Where pip put the command, which is a directory already on PATH.
|
|
111
|
+
|
|
112
|
+
The fallback is the USER scripts directory rather than the interpreter's
|
|
113
|
+
own: a Python under `C:\\Program Files` has a Scripts folder that needs
|
|
114
|
+
administrator rights to write to, so an alias written there fails for
|
|
115
|
+
exactly the people who did not install Python themselves. The user
|
|
116
|
+
scheme is where `pip install --user` puts a command anyway.
|
|
117
|
+
"""
|
|
118
|
+
found = shutil.which(branding.EXE_NAME) or shutil.which(
|
|
119
|
+
branding.EXE_NAME_GUI)
|
|
120
|
+
if found:
|
|
121
|
+
return os.path.dirname(found)
|
|
122
|
+
import sysconfig
|
|
123
|
+
try:
|
|
124
|
+
path = sysconfig.get_path("scripts", "nt_user")
|
|
125
|
+
if path:
|
|
126
|
+
return path
|
|
127
|
+
except (KeyError, ValueError):
|
|
128
|
+
pass
|
|
129
|
+
return os.path.join(os.path.dirname(sys.executable), "Scripts")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ----------------------------------------------------------------- POSIX
|
|
133
|
+
def _desktop_entry_dir():
|
|
134
|
+
base = os.environ.get("XDG_DATA_HOME") or os.path.expanduser(
|
|
135
|
+
"~/.local/share")
|
|
136
|
+
return os.path.join(base, "applications")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _write_desktop_entry(path, command, name, description):
|
|
140
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
141
|
+
fh.write("[Desktop Entry]\n")
|
|
142
|
+
fh.write("Type=Application\n")
|
|
143
|
+
fh.write("Name={}\n".format(name))
|
|
144
|
+
fh.write("Comment={}\n".format(description))
|
|
145
|
+
fh.write("Exec={} %F\n".format(" ".join(command)))
|
|
146
|
+
fh.write("Terminal=false\n")
|
|
147
|
+
fh.write("Categories=Science;Education;\n")
|
|
148
|
+
return path
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------------- API
|
|
152
|
+
def register(desktop=False, dry_run=False):
|
|
153
|
+
"""Make the program findable: Start Menu, or a .desktop entry."""
|
|
154
|
+
manifest = _load()
|
|
155
|
+
command = launcher()
|
|
156
|
+
made = []
|
|
157
|
+
if sys.platform == "win32":
|
|
158
|
+
link = os.path.join(_windows_start_menu(),
|
|
159
|
+
branding.APP_NAME + ".lnk")
|
|
160
|
+
made.append(("start menu", link))
|
|
161
|
+
if desktop:
|
|
162
|
+
made.append(("desktop", os.path.join(
|
|
163
|
+
os.path.expanduser("~/Desktop"),
|
|
164
|
+
branding.APP_NAME + ".lnk")))
|
|
165
|
+
if not dry_run:
|
|
166
|
+
for kind, path in made:
|
|
167
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
168
|
+
_make_shortcut(path, command, branding.DESCRIPTION)
|
|
169
|
+
_record(manifest, kind, path, branding.APP_NAME)
|
|
170
|
+
else:
|
|
171
|
+
path = os.path.join(_desktop_entry_dir(),
|
|
172
|
+
branding.EXE_NAME + ".desktop")
|
|
173
|
+
made.append(("desktop entry", path))
|
|
174
|
+
if not dry_run:
|
|
175
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
176
|
+
_write_desktop_entry(path, command, branding.APP_NAME,
|
|
177
|
+
branding.DESCRIPTION)
|
|
178
|
+
_record(manifest, "desktop entry", path, branding.APP_NAME)
|
|
179
|
+
if not dry_run:
|
|
180
|
+
_save(manifest)
|
|
181
|
+
return made
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def unregister(names=None, dry_run=False):
|
|
185
|
+
"""Remove what was registered, for this name or for the ones given."""
|
|
186
|
+
manifest = _load()
|
|
187
|
+
wanted = set(names or [branding.APP_NAME])
|
|
188
|
+
removed, kept = [], []
|
|
189
|
+
for entry in manifest["entries"]:
|
|
190
|
+
if entry.get("app") not in wanted:
|
|
191
|
+
kept.append(entry)
|
|
192
|
+
continue
|
|
193
|
+
path = entry.get("path", "")
|
|
194
|
+
removed.append(path)
|
|
195
|
+
if not dry_run and os.path.exists(path):
|
|
196
|
+
try:
|
|
197
|
+
os.remove(path)
|
|
198
|
+
except OSError:
|
|
199
|
+
kept.append(entry)
|
|
200
|
+
if not dry_run:
|
|
201
|
+
manifest["entries"] = kept
|
|
202
|
+
_save(manifest)
|
|
203
|
+
return removed
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def clean_legacy(dry_run=False):
|
|
207
|
+
"""Take away what the program's PREVIOUS names left behind.
|
|
208
|
+
|
|
209
|
+
This is the deregistration half of a rename. `branding.LEGACY_NAMES` is
|
|
210
|
+
the list a renamed release ships; everything here works from it, so the
|
|
211
|
+
old name never has to be remembered by hand.
|
|
212
|
+
"""
|
|
213
|
+
names = [entry[0] for entry in branding.LEGACY_NAMES]
|
|
214
|
+
removed = unregister(names, dry_run=dry_run) if names else []
|
|
215
|
+
# Shortcuts made before the manifest existed, or by a version that could
|
|
216
|
+
# not write it, are looked for by name as well.
|
|
217
|
+
for app_name, exe_name, _prog_id in branding.LEGACY_NAMES:
|
|
218
|
+
guesses = []
|
|
219
|
+
if sys.platform == "win32":
|
|
220
|
+
guesses.append(os.path.join(_windows_start_menu(),
|
|
221
|
+
app_name + ".lnk"))
|
|
222
|
+
guesses.append(os.path.join(os.path.expanduser("~/Desktop"),
|
|
223
|
+
app_name + ".lnk"))
|
|
224
|
+
guesses.append(os.path.join(_windows_scripts_dir(),
|
|
225
|
+
exe_name + ".cmd"))
|
|
226
|
+
else:
|
|
227
|
+
guesses.append(os.path.join(_desktop_entry_dir(),
|
|
228
|
+
exe_name + ".desktop"))
|
|
229
|
+
guesses.append(os.path.expanduser("~/.local/bin/" + exe_name))
|
|
230
|
+
for path in guesses:
|
|
231
|
+
if os.path.exists(path):
|
|
232
|
+
removed.append(path)
|
|
233
|
+
if not dry_run:
|
|
234
|
+
try:
|
|
235
|
+
os.remove(path)
|
|
236
|
+
except OSError:
|
|
237
|
+
pass
|
|
238
|
+
return removed
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def alias(name, remove=False, dry_run=False):
|
|
242
|
+
"""Make `name` start the program too, and remember that it was made."""
|
|
243
|
+
manifest = _load()
|
|
244
|
+
command = launcher()
|
|
245
|
+
if sys.platform == "win32":
|
|
246
|
+
path = os.path.join(_windows_scripts_dir(), name + ".cmd")
|
|
247
|
+
body = '@echo off\r\n"{}" %*\r\n'.format(" ".join(command))
|
|
248
|
+
else:
|
|
249
|
+
path = os.path.expanduser(os.path.join("~/.local/bin", name))
|
|
250
|
+
body = "#!/bin/sh\nexec {} \"$@\"\n".format(" ".join(command))
|
|
251
|
+
if remove:
|
|
252
|
+
if not dry_run and os.path.exists(path):
|
|
253
|
+
os.remove(path)
|
|
254
|
+
manifest["entries"] = [e for e in manifest["entries"]
|
|
255
|
+
if e.get("path") != path]
|
|
256
|
+
if not dry_run:
|
|
257
|
+
_save(manifest)
|
|
258
|
+
return path
|
|
259
|
+
if not dry_run:
|
|
260
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
261
|
+
with open(path, "w", encoding="utf-8", newline="") as fh:
|
|
262
|
+
fh.write(body)
|
|
263
|
+
if sys.platform != "win32":
|
|
264
|
+
os.chmod(path, 0o755)
|
|
265
|
+
_record(manifest, "alias", path, branding.APP_NAME)
|
|
266
|
+
_save(manifest)
|
|
267
|
+
return path
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def aliases():
|
|
271
|
+
"""Every alias this program knows it made."""
|
|
272
|
+
return [e["path"] for e in _load()["entries"] if e.get("kind") == "alias"]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def describe():
|
|
276
|
+
"""What is registered right now, as lines for a terminal."""
|
|
277
|
+
manifest = _load()
|
|
278
|
+
if not manifest["entries"]:
|
|
279
|
+
return ["nothing registered (run: {} register)".format(
|
|
280
|
+
branding.EXE_NAME)]
|
|
281
|
+
return ["{:<14} {}{}".format(e.get("kind", "?"), e.get("path", "?"),
|
|
282
|
+
"" if os.path.exists(e.get("path", ""))
|
|
283
|
+
else " (gone)")
|
|
284
|
+
for e in manifest["entries"]]
|
dscpanel/ui/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The rest of the window, in the plot's colours.
|
|
2
|
+
|
|
3
|
+
The plot paints its own theme (`ui/plot.py`, `THEMES`). Everything else - the
|
|
4
|
+
outliner, the menus, the status bar, every dialog - is Qt's, and it followed
|
|
5
|
+
the system's light look while the plot beside it was dark. When the plot
|
|
6
|
+
is dark, so should every window be.
|
|
7
|
+
|
|
8
|
+
So the application palette follows the plot's theme: dark greys (Fusion
|
|
9
|
+
style plus a Blender-ish palette) for `blender-default`, Fusion's own
|
|
10
|
+
light palette for `light`, the Boombox skin for `boombox`. Fusion in every
|
|
11
|
+
case, because a palette only fully applies under a style that draws with
|
|
12
|
+
it, and switching styles between two themes would change the widgets'
|
|
13
|
+
shapes as well as their colours.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import Qt
|
|
17
|
+
from PySide6.QtGui import QColor, QGuiApplication, QPalette
|
|
18
|
+
from PySide6.QtWidgets import QApplication
|
|
19
|
+
|
|
20
|
+
from . import plot as plot_module
|
|
21
|
+
|
|
22
|
+
#: The theme the application palette was last set for, so a refresh that
|
|
23
|
+
#: changes nothing does not repaint every widget in the program.
|
|
24
|
+
_applied = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def dark_palette():
|
|
28
|
+
"""The dark palette: Fusion's greys, Blender-ish."""
|
|
29
|
+
p = QPalette()
|
|
30
|
+
grey = QColor(53, 53, 53)
|
|
31
|
+
base = QColor(42, 42, 42)
|
|
32
|
+
text = QColor(220, 220, 220)
|
|
33
|
+
disabled = QColor(128, 128, 128)
|
|
34
|
+
p.setColor(QPalette.Window, grey)
|
|
35
|
+
p.setColor(QPalette.WindowText, text)
|
|
36
|
+
p.setColor(QPalette.Base, base)
|
|
37
|
+
p.setColor(QPalette.AlternateBase, QColor(58, 58, 58))
|
|
38
|
+
p.setColor(QPalette.ToolTipBase, QColor(42, 42, 42))
|
|
39
|
+
p.setColor(QPalette.ToolTipText, text)
|
|
40
|
+
p.setColor(QPalette.PlaceholderText, QColor(140, 140, 140))
|
|
41
|
+
p.setColor(QPalette.Text, text)
|
|
42
|
+
p.setColor(QPalette.Button, grey)
|
|
43
|
+
p.setColor(QPalette.ButtonText, text)
|
|
44
|
+
p.setColor(QPalette.BrightText, QColor(255, 120, 120))
|
|
45
|
+
p.setColor(QPalette.Link, QColor(110, 160, 220))
|
|
46
|
+
p.setColor(QPalette.Highlight, QColor(70, 105, 150))
|
|
47
|
+
p.setColor(QPalette.HighlightedText, QColor(240, 240, 240))
|
|
48
|
+
for role in (QPalette.WindowText, QPalette.Text, QPalette.ButtonText):
|
|
49
|
+
p.setColor(QPalette.Disabled, role, disabled)
|
|
50
|
+
p.setColor(QPalette.Disabled, QPalette.Highlight, QColor(80, 80, 80))
|
|
51
|
+
return p
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def light_palette():
|
|
55
|
+
"""Fusion's classic light palette, written out.
|
|
56
|
+
|
|
57
|
+
Not `standardPalette()`: that follows the colour scheme in force at the
|
|
58
|
+
moment it is asked, so taken while the dark scheme was still set it
|
|
59
|
+
handed back a DARK palette and the "light" theme stayed dark."""
|
|
60
|
+
p = QPalette()
|
|
61
|
+
window = QColor(239, 239, 239)
|
|
62
|
+
text = QColor(20, 20, 20)
|
|
63
|
+
disabled = QColor(150, 150, 150)
|
|
64
|
+
p.setColor(QPalette.Window, window)
|
|
65
|
+
p.setColor(QPalette.WindowText, text)
|
|
66
|
+
p.setColor(QPalette.Base, QColor(255, 255, 255))
|
|
67
|
+
p.setColor(QPalette.AlternateBase, QColor(245, 245, 245))
|
|
68
|
+
p.setColor(QPalette.ToolTipBase, QColor(255, 255, 220))
|
|
69
|
+
p.setColor(QPalette.ToolTipText, text)
|
|
70
|
+
p.setColor(QPalette.PlaceholderText, QColor(120, 120, 120))
|
|
71
|
+
p.setColor(QPalette.Text, text)
|
|
72
|
+
p.setColor(QPalette.Button, window)
|
|
73
|
+
p.setColor(QPalette.ButtonText, text)
|
|
74
|
+
p.setColor(QPalette.BrightText, QColor(200, 30, 30))
|
|
75
|
+
p.setColor(QPalette.Link, QColor(30, 90, 180))
|
|
76
|
+
p.setColor(QPalette.Highlight, QColor(48, 140, 198))
|
|
77
|
+
p.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
|
78
|
+
for role in (QPalette.WindowText, QPalette.Text, QPalette.ButtonText):
|
|
79
|
+
p.setColor(QPalette.Disabled, role, disabled)
|
|
80
|
+
return p
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def boombox_palette():
|
|
84
|
+
"""The Boombox skin: brushed-metal greys, parchment text, the LCD
|
|
85
|
+
green."""
|
|
86
|
+
p = QPalette()
|
|
87
|
+
window = QColor("#232323")
|
|
88
|
+
text = QColor("#d6d6c2")
|
|
89
|
+
disabled = QColor("#6b7178")
|
|
90
|
+
p.setColor(QPalette.Window, window)
|
|
91
|
+
p.setColor(QPalette.WindowText, text)
|
|
92
|
+
p.setColor(QPalette.Base, QColor("#2a2d31"))
|
|
93
|
+
p.setColor(QPalette.AlternateBase, QColor("#2e3236"))
|
|
94
|
+
p.setColor(QPalette.ToolTipBase, QColor("#0c130c"))
|
|
95
|
+
p.setColor(QPalette.ToolTipText, QColor("#39ff7a"))
|
|
96
|
+
p.setColor(QPalette.PlaceholderText, QColor("#9a9a86"))
|
|
97
|
+
p.setColor(QPalette.Text, text)
|
|
98
|
+
p.setColor(QPalette.Button, QColor("#3a3f44"))
|
|
99
|
+
p.setColor(QPalette.ButtonText, text)
|
|
100
|
+
p.setColor(QPalette.BrightText, QColor("#ff5555"))
|
|
101
|
+
p.setColor(QPalette.Link, QColor("#39ff7a"))
|
|
102
|
+
p.setColor(QPalette.Highlight, QColor("#2f7d4a"))
|
|
103
|
+
p.setColor(QPalette.HighlightedText, QColor("#caffd9"))
|
|
104
|
+
p.setColor(QPalette.Mid, QColor("#15171a"))
|
|
105
|
+
for role in (QPalette.WindowText, QPalette.Text, QPalette.ButtonText):
|
|
106
|
+
p.setColor(QPalette.Disabled, role, disabled)
|
|
107
|
+
p.setColor(QPalette.Disabled, QPalette.Highlight, QColor("#3a3f44"))
|
|
108
|
+
return p
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
#: The window's palette for each of the plot's themes.
|
|
112
|
+
PALETTES = {plot_module.THEME_DARK: dark_palette,
|
|
113
|
+
plot_module.THEME_LIGHT: light_palette,
|
|
114
|
+
plot_module.THEME_BOOMBOX: boombox_palette}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def apply(theme, app=None):
|
|
118
|
+
"""Give the whole application the palette that goes with `theme`.
|
|
119
|
+
|
|
120
|
+
Returns True when anything changed. Also tells Qt which colour scheme is
|
|
121
|
+
in force where it can be told (Qt 6.8+), which is what darkens the
|
|
122
|
+
Windows title bar to match.
|
|
123
|
+
"""
|
|
124
|
+
global _applied
|
|
125
|
+
app = app or QApplication.instance()
|
|
126
|
+
if app is None or theme == _applied:
|
|
127
|
+
return False
|
|
128
|
+
if app.style().name().lower() != "fusion":
|
|
129
|
+
app.setStyle("Fusion")
|
|
130
|
+
dark = theme != plot_module.THEME_LIGHT
|
|
131
|
+
# The scheme FIRST, the palette after it: a scheme change makes Qt
|
|
132
|
+
# re-derive colours, and the explicit palette has to be the last word.
|
|
133
|
+
hints = QGuiApplication.styleHints()
|
|
134
|
+
try:
|
|
135
|
+
hints.setColorScheme(Qt.ColorScheme.Dark if dark
|
|
136
|
+
else Qt.ColorScheme.Light)
|
|
137
|
+
except AttributeError:
|
|
138
|
+
pass # an older Qt: the title bar stays as it is
|
|
139
|
+
app.setPalette(PALETTES.get(theme, dark_palette)())
|
|
140
|
+
_applied = theme
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def applied():
|
|
145
|
+
"""The theme the application is currently dressed in. For tests."""
|
|
146
|
+
return _applied
|