visualdynamics 0.1.0a1__tar.gz → 0.1.0a2__tar.gz
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.
- {visualdynamics-0.1.0a1/src/visualdynamics.egg-info → visualdynamics-0.1.0a2}/PKG-INFO +1 -1
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/__init__.py +1 -1
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/__init__.py +31 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/main_window.py +61 -7
- visualdynamics-0.1.0a2/src/visualdynamics/gui/preferences.py +135 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/theme.py +73 -2
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2/src/visualdynamics.egg-info}/PKG-INFO +1 -1
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics.egg-info/SOURCES.txt +1 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/LICENSE +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/README.md +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/pyproject.toml +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/setup.cfg +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/__main__.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/compatibility.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/__init__.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/author.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/averaging.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/channel_table.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/compliance.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/correlate.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/data.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/detect.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/entities.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/fem.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/filters.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/geometry.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/kurtosis.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/matches.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/merge.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/modal_fit.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/octave.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/photos.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/replication.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/report.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/rigid.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/shapes.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/shocks.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/sine.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/srs.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/tables.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/transform.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/truncate.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/unit_choices.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/validate.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/core/wavelet.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/decimate.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/deform.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/demo/__init__.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/demo/drone.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/demo/plate.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/app_icon.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/author_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/averaging_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/console.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/disclaimer.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/drawer_tab.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/editors.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/filter_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/glyphs.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/icons.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/object_tables.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/octave_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/panes.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/project_tree.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/record_grid.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/report_editor.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/rigid_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/settings_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/shock_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/stage_drag.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/tables.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/toolbars.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/truncate_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/wavelet_panel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/windows.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/__init__.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/adf.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/excel.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/exodus.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/exporters.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/femap.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/nastran.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/native.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/photo_files.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/punch.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/rattlesnake.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/report_template.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/sdynpy_data.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/sdynpy_npz.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/sep005.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/sniffing.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/step.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/stl.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/threemf.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/unv.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/names.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/__init__.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/averaging.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/bands.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/bars.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/filtering.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/scalogram.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/shocks.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/plot/truncation.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/project.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/py.typed +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/report/__init__.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/report/markdown.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/report/page.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/rotate.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/units.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/update.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/__init__.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/_quiet.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/animate.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/banded.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/geometry.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/mac_bars.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/marks.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/paired.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/pick.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/rigid.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/scalogram.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/sinespec.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/viz/waterfall.py +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics.egg-info/entry_points.txt +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics.egg-info/requires.txt +0 -0
- {visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics.egg-info/top_level.txt +0 -0
|
@@ -76,7 +76,7 @@ from .units import (
|
|
|
76
76
|
#: alpha, said in the version itself so the dialog, the About text, the
|
|
77
77
|
#: installers and latest.json all say the same thing, and the first
|
|
78
78
|
#: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
|
|
79
|
-
__version__ = '0.1.
|
|
79
|
+
__version__ = '0.1.0a2'
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
|
|
@@ -92,6 +92,30 @@ def check_qt_binding() -> None:
|
|
|
92
92
|
f'which then breaks sdynpy\'s own plotting in that process.')
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def theme_flag(argv: list[str]) -> tuple[list[str], str | None]:
|
|
96
|
+
"""Take `--theme dark`, `--theme light` or `--theme=dark` out of
|
|
97
|
+
the arguments: the rest, and the theme named, or None."""
|
|
98
|
+
rest: list[str] = []
|
|
99
|
+
theme = None
|
|
100
|
+
skip = False
|
|
101
|
+
for i, arg in enumerate(argv):
|
|
102
|
+
if skip:
|
|
103
|
+
skip = False
|
|
104
|
+
continue
|
|
105
|
+
if arg.startswith('--theme='):
|
|
106
|
+
theme = arg.split('=', 1)[1]
|
|
107
|
+
elif arg == '--theme' and i + 1 < len(argv):
|
|
108
|
+
theme = argv[i + 1]
|
|
109
|
+
skip = True
|
|
110
|
+
else:
|
|
111
|
+
rest.append(arg)
|
|
112
|
+
if theme is not None:
|
|
113
|
+
theme = theme.strip().lower()
|
|
114
|
+
if theme not in ('dark', 'light'):
|
|
115
|
+
raise SystemExit(f"--theme takes dark or light, not {theme!r}")
|
|
116
|
+
return rest, theme
|
|
117
|
+
|
|
118
|
+
|
|
95
119
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
96
120
|
import os
|
|
97
121
|
|
|
@@ -108,6 +132,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
108
132
|
|
|
109
133
|
check_qt_binding() # before a window is built out of the mismatch
|
|
110
134
|
|
|
135
|
+
from ..theme import OVERRIDE as THEME_OVERRIDE
|
|
111
136
|
from .main_window import MainWindow
|
|
112
137
|
|
|
113
138
|
# let Ctrl-C in the launching terminal kill the GUI immediately instead
|
|
@@ -121,6 +146,12 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
121
146
|
# its own and is not a file to import
|
|
122
147
|
with_disclaimer = '--no-disclaimer' not in argv
|
|
123
148
|
argv = [a for a in argv if a != '--no-disclaimer']
|
|
149
|
+
# --theme dark|light: said into the environment, which is the one
|
|
150
|
+
# place the theme module looks first, so the window and every
|
|
151
|
+
# later `apply_theme` agree (2026-09-14)
|
|
152
|
+
argv, theme = theme_flag(argv)
|
|
153
|
+
if theme is not None:
|
|
154
|
+
os.environ[THEME_OVERRIDE] = theme
|
|
124
155
|
# what the machine calls this while it is running — and it must be
|
|
125
156
|
# said *before* the QApplication exists. Cocoa's application menu
|
|
126
157
|
# ('Hide …', 'Quit …') is built during construction, titled from
|
|
@@ -134,7 +134,7 @@ from ..rotate import (
|
|
|
134
134
|
rotate_frame,
|
|
135
135
|
wrapped,
|
|
136
136
|
)
|
|
137
|
-
from ..theme import OVERLAY_ALPHA
|
|
137
|
+
from ..theme import OVERLAY_ALPHA
|
|
138
138
|
from ..theme import theme as resolve_theme
|
|
139
139
|
from ..units import DEFAULT_SYSTEM, SYSTEMS, UnitSystem
|
|
140
140
|
from ..viz.geometry import (
|
|
@@ -177,6 +177,13 @@ from .object_tables import (
|
|
|
177
177
|
units_table_model,
|
|
178
178
|
)
|
|
179
179
|
from .panes import DataPane, ScenePane
|
|
180
|
+
from .preferences import (
|
|
181
|
+
chosen_scheme,
|
|
182
|
+
refresh_palettes,
|
|
183
|
+
remember_appearance,
|
|
184
|
+
remembered_appearance,
|
|
185
|
+
wear_appearance,
|
|
186
|
+
)
|
|
180
187
|
from .project_tree import (
|
|
181
188
|
PROJECT_ROW,
|
|
182
189
|
ROLE_ACTIVE,
|
|
@@ -649,8 +656,12 @@ class MainWindow(QMainWindow):
|
|
|
649
656
|
check_qt_binding() # a mismatch is unreadable once it reaches a layout
|
|
650
657
|
import pyqtgraph as pg
|
|
651
658
|
|
|
652
|
-
#
|
|
653
|
-
|
|
659
|
+
# the appearance chosen for this launch, remembered, or the
|
|
660
|
+
# platform's — in that order (gui/preferences.py) — worn by the
|
|
661
|
+
# whole application first, so the chrome and the drawn parts
|
|
662
|
+
# are built to the same scheme
|
|
663
|
+
wear_appearance()
|
|
664
|
+
self.theme_name: str = chosen_scheme()
|
|
654
665
|
colors = resolve_theme(self.theme_name)
|
|
655
666
|
pg.setConfigOption('background', colors['plot_background'])
|
|
656
667
|
pg.setConfigOption('foreground', colors['plot_foreground'])
|
|
@@ -1343,6 +1354,25 @@ class MainWindow(QMainWindow):
|
|
|
1343
1354
|
report_menu.addAction('&Empty',
|
|
1344
1355
|
lambda: self.generate_report('empty'))
|
|
1345
1356
|
file_menu.addSeparator()
|
|
1357
|
+
# light, dark, or the platform's choice, remembered between
|
|
1358
|
+
# launches: a Linux desktop Qt could not read left a friend of
|
|
1359
|
+
# Brandon's with a light window and no way to change it
|
|
1360
|
+
# (2026-09-14). One menu, still — File stays the only one.
|
|
1361
|
+
appearance = file_menu.addMenu('&Appearance')
|
|
1362
|
+
group = QActionGroup(self)
|
|
1363
|
+
group.setExclusive(True)
|
|
1364
|
+
self.appearance_actions: dict[str, QAction] = {}
|
|
1365
|
+
remembered = remembered_appearance()
|
|
1366
|
+
for choice, label in (('system', '&System'), ('light', '&Light'),
|
|
1367
|
+
('dark', '&Dark')):
|
|
1368
|
+
action = appearance.addAction(label)
|
|
1369
|
+
action.setCheckable(True)
|
|
1370
|
+
action.setChecked(choice == remembered)
|
|
1371
|
+
action.triggered.connect(
|
|
1372
|
+
lambda _checked=False, choice=choice:
|
|
1373
|
+
self.choose_appearance(choice))
|
|
1374
|
+
group.addAction(action)
|
|
1375
|
+
self.appearance_actions[choice] = action
|
|
1346
1376
|
file_menu.addAction('Check for &Updates...', self.check_for_updates)
|
|
1347
1377
|
# the About role: macOS moves it into the application menu,
|
|
1348
1378
|
# where a Mac user looks for a version; Windows and Linux keep
|
|
@@ -2096,19 +2126,43 @@ class MainWindow(QMainWindow):
|
|
|
2096
2126
|
self.dofs_combo_action.setVisible(bool(quantities))
|
|
2097
2127
|
|
|
2098
2128
|
def _scheme_changed(self, _scheme) -> None:
|
|
2099
|
-
"""The platform switched light and dark: restate the theme
|
|
2129
|
+
"""The platform switched light and dark: restate the theme —
|
|
2130
|
+
which follows it only while the appearance is System — and
|
|
2131
|
+
make every widget re-read the palette, which they do not do
|
|
2132
|
+
on their own (gui/preferences.py, `refresh_palettes`)."""
|
|
2133
|
+
refresh_palettes()
|
|
2100
2134
|
self.apply_theme()
|
|
2101
2135
|
|
|
2136
|
+
def choose_appearance(self, choice: str) -> None:
|
|
2137
|
+
"""File → Appearance: remember the choice and wear it now.
|
|
2138
|
+
'system' means follow the platform again."""
|
|
2139
|
+
remember_appearance(choice)
|
|
2140
|
+
for name, action in self.appearance_actions.items():
|
|
2141
|
+
action.setChecked(name == choice)
|
|
2142
|
+
# the application first — chrome, menus, native widgets — then
|
|
2143
|
+
# the parts drawn here; Qt announces the switch through
|
|
2144
|
+
# colorSchemeChanged too, which lands on apply_theme as well
|
|
2145
|
+
wear_appearance(choice=choice)
|
|
2146
|
+
self.apply_theme()
|
|
2147
|
+
self._show_status(
|
|
2148
|
+
'Following the system appearance' if choice == 'system'
|
|
2149
|
+
else f'{choice.capitalize()} appearance, remembered')
|
|
2150
|
+
|
|
2102
2151
|
def apply_theme(self, name: str | None = None) -> None:
|
|
2103
|
-
"""Adopt a light/dark theme (default:
|
|
2152
|
+
"""Adopt a light/dark theme (default: the one chosen — this
|
|
2153
|
+
launch's flag, the remembered appearance, or the OS)."""
|
|
2104
2154
|
import pyqtgraph as pg
|
|
2105
2155
|
|
|
2106
|
-
self.theme_name = name or
|
|
2156
|
+
self.theme_name = name or chosen_scheme()
|
|
2107
2157
|
colors = resolve_theme(self.theme_name)
|
|
2108
2158
|
_keep_selection_vivid(self.table)
|
|
2109
2159
|
pg.setConfigOption('foreground', colors['plot_foreground'])
|
|
2110
2160
|
self.data_pane.apply_theme(self.theme_name, colors)
|
|
2111
|
-
|
|
2161
|
+
# the scene's own theme name, not just its background: told
|
|
2162
|
+
# only to repaint, it repainted in the theme it was built with,
|
|
2163
|
+
# and a window opened light stayed white after Dark (Brandon's
|
|
2164
|
+
# screenshot, 2026-09-14)
|
|
2165
|
+
self.scene.apply_theme(self.theme_name)
|
|
2112
2166
|
# the tree wears the scene's own ground — one theme for every
|
|
2113
2167
|
# surface, black or white, never the platform's grey (Brandon,
|
|
2114
2168
|
# 2026-08-23)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""What the application remembers between launches: today, the
|
|
2
|
+
appearance.
|
|
3
|
+
|
|
4
|
+
One `QSettings` under one name, so a preference set on one launch is
|
|
5
|
+
read on the next; the tests point the store at a temporary folder
|
|
6
|
+
through `QSettings.setPath`, so nothing a test chooses reaches the
|
|
7
|
+
user's own file.
|
|
8
|
+
|
|
9
|
+
The appearance a window wears is decided in this order, and the order
|
|
10
|
+
is the point: an explicit statement for this launch
|
|
11
|
+
(`VISUALDYNAMICS_THEME`, which `--theme` sets) beats the remembered
|
|
12
|
+
choice, and the remembered choice beats what the platform reports —
|
|
13
|
+
`'system'` means follow the platform, which is the default and what
|
|
14
|
+
`theme.system_scheme` answers.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from PySide6.QtCore import QSettings
|
|
23
|
+
|
|
24
|
+
from ..theme import OVERRIDE, THEMES, system_scheme
|
|
25
|
+
|
|
26
|
+
#: the remembered choices, in menu order
|
|
27
|
+
APPEARANCES = ('system', 'light', 'dark')
|
|
28
|
+
KEY = 'appearance'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#: a file to keep the preferences in instead of the platform's store —
|
|
32
|
+
#: what the tests set, so nothing a test chooses reaches the user's own
|
|
33
|
+
STORE = 'VISUALDYNAMICS_SETTINGS'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def settings() -> QSettings:
|
|
37
|
+
"""The application's own store: the platform's (a plist on macOS,
|
|
38
|
+
the registry on Windows, a file under ~/.config on Linux), or the
|
|
39
|
+
file `VISUALDYNAMICS_SETTINGS` names."""
|
|
40
|
+
path = os.environ.get(STORE)
|
|
41
|
+
if path:
|
|
42
|
+
return QSettings(path, QSettings.Format.IniFormat)
|
|
43
|
+
return QSettings('visualdynamics', 'Visual Dynamics')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def remembered_appearance() -> str:
|
|
47
|
+
"""'system', 'light' or 'dark' — 'system' when nothing was chosen
|
|
48
|
+
or the stored word is not one of ours."""
|
|
49
|
+
said = str(settings().value(KEY, 'system')).strip().lower()
|
|
50
|
+
return said if said in APPEARANCES else 'system'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def remember_appearance(choice: str) -> None:
|
|
54
|
+
"""Store the choice; 'system' clears it rather than storing a word
|
|
55
|
+
that means "nothing chosen"."""
|
|
56
|
+
if choice not in APPEARANCES:
|
|
57
|
+
raise ValueError(f'appearance is one of {APPEARANCES}, not {choice!r}')
|
|
58
|
+
store = settings()
|
|
59
|
+
if choice == 'system':
|
|
60
|
+
store.remove(KEY)
|
|
61
|
+
else:
|
|
62
|
+
store.setValue(KEY, choice)
|
|
63
|
+
store.sync()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def chosen_scheme() -> str:
|
|
67
|
+
"""The theme to wear now: this launch's statement, else the
|
|
68
|
+
remembered choice, else the platform's."""
|
|
69
|
+
said = os.environ.get(OVERRIDE, '').strip().lower()
|
|
70
|
+
if said in THEMES:
|
|
71
|
+
return said
|
|
72
|
+
remembered = remembered_appearance()
|
|
73
|
+
if remembered in THEMES:
|
|
74
|
+
return remembered
|
|
75
|
+
return system_scheme()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def wear_appearance(app: Any = None, choice: str | None = None) -> None:
|
|
79
|
+
"""Make the whole application wear the chosen appearance — the
|
|
80
|
+
window chrome, the menus, every native widget — not only the parts
|
|
81
|
+
this program draws itself.
|
|
82
|
+
|
|
83
|
+
Qt 6.8 gave `QStyleHints.setColorScheme`: Light or Dark forces the
|
|
84
|
+
application's appearance (on macOS, the NSApp appearance), Unknown
|
|
85
|
+
follows the platform again. Without it the first cut of the menu
|
|
86
|
+
changed the plots and the scene and left the chrome as the OS had
|
|
87
|
+
it, and Brandon chose Light and saw nothing change (2026-09-14).
|
|
88
|
+
The platform then reports the forced scheme, which is what
|
|
89
|
+
`theme.system_scheme` and the `colorSchemeChanged` slot read, so
|
|
90
|
+
every later repaint agrees. `choice` defaults to what
|
|
91
|
+
`chosen_scheme` would answer: this launch's statement, else the
|
|
92
|
+
remembered choice, else the platform's.
|
|
93
|
+
"""
|
|
94
|
+
from PySide6.QtCore import Qt
|
|
95
|
+
from PySide6.QtWidgets import QApplication
|
|
96
|
+
|
|
97
|
+
app = app or QApplication.instance()
|
|
98
|
+
if app is None:
|
|
99
|
+
return
|
|
100
|
+
hints = app.styleHints()
|
|
101
|
+
if not hasattr(hints, 'setColorScheme'):
|
|
102
|
+
return # an older Qt: the drawn parts only
|
|
103
|
+
if choice is None:
|
|
104
|
+
said = os.environ.get(OVERRIDE, '').strip().lower()
|
|
105
|
+
choice = said if said in THEMES else remembered_appearance()
|
|
106
|
+
hints.setColorScheme({'light': Qt.ColorScheme.Light,
|
|
107
|
+
'dark': Qt.ColorScheme.Dark}.get(
|
|
108
|
+
choice, Qt.ColorScheme.Unknown))
|
|
109
|
+
refresh_palettes(app)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def refresh_palettes(app: Any = None) -> None:
|
|
113
|
+
"""Make every widget re-read the application palette.
|
|
114
|
+
|
|
115
|
+
On macOS with Qt 6.11 the application palette follows a colour
|
|
116
|
+
scheme change at once, but widgets already on screen keep the
|
|
117
|
+
palette they resolved before it — the status bar and the panes
|
|
118
|
+
stayed light after Brandon switched back to Dark (his screenshot,
|
|
119
|
+
2026-09-14), while a fresh window came up right. Setting an
|
|
120
|
+
*empty* palette on a widget makes it resolve from its parent and
|
|
121
|
+
the application again, and leaves `WA_SetPalette` clear, so the
|
|
122
|
+
widgets that own a palette on purpose (the tree and the tables,
|
|
123
|
+
coloured by the theme) are skipped and keep theirs.
|
|
124
|
+
"""
|
|
125
|
+
from PySide6.QtCore import Qt
|
|
126
|
+
from PySide6.QtGui import QPalette
|
|
127
|
+
from PySide6.QtWidgets import QApplication, QWidget
|
|
128
|
+
|
|
129
|
+
app = app or QApplication.instance()
|
|
130
|
+
if app is None or not hasattr(app, 'topLevelWidgets'):
|
|
131
|
+
return
|
|
132
|
+
for top in app.topLevelWidgets():
|
|
133
|
+
for widget in (top, *top.findChildren(QWidget)):
|
|
134
|
+
if not widget.testAttribute(Qt.WidgetAttribute.WA_SetPalette):
|
|
135
|
+
widget.setPalette(QPalette())
|
|
@@ -148,12 +148,23 @@ def colormap(values: Any) -> Any:
|
|
|
148
148
|
return stops[low] * (1.0 - f) + stops[low + 1] * f
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
#: an explicit answer beats every guess: `VISUALDYNAMICS_THEME=dark`
|
|
152
|
+
#: (or light) in the environment, which the launcher's `--theme` sets
|
|
153
|
+
OVERRIDE = 'VISUALDYNAMICS_THEME'
|
|
154
|
+
|
|
155
|
+
|
|
151
156
|
def system_scheme(app: Any = None) -> str:
|
|
152
|
-
"""'dark' or 'light'
|
|
157
|
+
"""'dark' or 'light': the environment's say, else the OS appearance
|
|
158
|
+
via Qt, else the desktop asked directly, else the palette.
|
|
153
159
|
|
|
154
160
|
Falls back to the default theme when Qt or an application instance is
|
|
155
161
|
unavailable (e.g. plain scripting use).
|
|
156
162
|
"""
|
|
163
|
+
import os
|
|
164
|
+
|
|
165
|
+
said = os.environ.get(OVERRIDE, '').strip().lower()
|
|
166
|
+
if said in THEMES:
|
|
167
|
+
return said
|
|
157
168
|
try:
|
|
158
169
|
from PySide6.QtCore import Qt
|
|
159
170
|
from PySide6.QtWidgets import QApplication
|
|
@@ -167,6 +178,66 @@ def system_scheme(app: Any = None) -> str:
|
|
|
167
178
|
return 'dark'
|
|
168
179
|
if scheme == Qt.ColorScheme.Light:
|
|
169
180
|
return 'light'
|
|
170
|
-
|
|
181
|
+
# Qt could not tell. On Linux that is the usual case: Qt learns the
|
|
182
|
+
# scheme through a platform-theme plugin, and a packaged build
|
|
183
|
+
# shipped none until 2026-09-14 — a friend of Brandon's on a dark
|
|
184
|
+
# desktop got a light window. The desktop is asked directly before
|
|
185
|
+
# the palette is judged, since the palette is Qt's own light one
|
|
186
|
+
# whenever the plugin is missing.
|
|
187
|
+
asked = desktop_scheme()
|
|
188
|
+
if asked is not None:
|
|
189
|
+
return asked
|
|
190
|
+
palette = app.palette() # judge by palette lightness
|
|
171
191
|
return ('dark' if palette.color(palette.ColorRole.Window).lightness() < 128
|
|
172
192
|
else 'light')
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def desktop_scheme(platform: str | None = None,
|
|
196
|
+
run: Any = None, home: str | None = None) -> str | None:
|
|
197
|
+
"""The Linux desktop's own answer, or None where there is none.
|
|
198
|
+
|
|
199
|
+
Three doors, in the order a modern desktop answers them: the XDG
|
|
200
|
+
settings portal's `color-scheme` (GNOME, KDE and the rest, over
|
|
201
|
+
D-Bus through `gdbus`, which ships with GLib), GNOME's
|
|
202
|
+
`gsettings` key of the same name, and KDE's `kdeglobals`. Each is
|
|
203
|
+
a subprocess or a file read with a short timeout, and any failure
|
|
204
|
+
is "no answer" — never an exception in the way of a window.
|
|
205
|
+
`platform`, `run` and `home` are for the tests.
|
|
206
|
+
"""
|
|
207
|
+
import os
|
|
208
|
+
import subprocess
|
|
209
|
+
import sys
|
|
210
|
+
|
|
211
|
+
platform = platform or sys.platform
|
|
212
|
+
if not platform.startswith('linux'):
|
|
213
|
+
return None
|
|
214
|
+
run = run or (lambda cmd: subprocess.run(
|
|
215
|
+
cmd, capture_output=True, text=True, timeout=2, check=False).stdout)
|
|
216
|
+
for command, dark, light in (
|
|
217
|
+
(['gdbus', 'call', '--session',
|
|
218
|
+
'--dest', 'org.freedesktop.portal.Desktop',
|
|
219
|
+
'--object-path', '/org/freedesktop/portal/desktop',
|
|
220
|
+
'--method', 'org.freedesktop.portal.Settings.ReadOne',
|
|
221
|
+
'org.freedesktop.appearance', 'color-scheme'],
|
|
222
|
+
'uint32 1', 'uint32 2'),
|
|
223
|
+
(['gsettings', 'get', 'org.gnome.desktop.interface',
|
|
224
|
+
'color-scheme'],
|
|
225
|
+
'prefer-dark', 'prefer-light')):
|
|
226
|
+
try:
|
|
227
|
+
out = run(command) or ''
|
|
228
|
+
except Exception: # noqa: BLE001, S112 — absent tool, timeout: no answer from this door
|
|
229
|
+
continue
|
|
230
|
+
if dark in out:
|
|
231
|
+
return 'dark'
|
|
232
|
+
if light in out:
|
|
233
|
+
return 'light'
|
|
234
|
+
try:
|
|
235
|
+
kdeglobals = os.path.join(home or os.path.expanduser('~'),
|
|
236
|
+
'.config', 'kdeglobals')
|
|
237
|
+
with open(kdeglobals, encoding='utf-8', errors='replace') as f:
|
|
238
|
+
for line in f:
|
|
239
|
+
if line.strip().lower().startswith('colorscheme='):
|
|
240
|
+
return ('dark' if 'dark' in line.lower() else 'light')
|
|
241
|
+
except OSError:
|
|
242
|
+
pass
|
|
243
|
+
return None
|
|
@@ -68,6 +68,7 @@ src/visualdynamics/gui/main_window.py
|
|
|
68
68
|
src/visualdynamics/gui/object_tables.py
|
|
69
69
|
src/visualdynamics/gui/octave_panel.py
|
|
70
70
|
src/visualdynamics/gui/panes.py
|
|
71
|
+
src/visualdynamics/gui/preferences.py
|
|
71
72
|
src/visualdynamics/gui/project_tree.py
|
|
72
73
|
src/visualdynamics/gui/record_grid.py
|
|
73
74
|
src/visualdynamics/gui/report_editor.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/icons/drawer-tab-shape.svg
RENAMED
|
File without changes
|
{visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/gui/icons/drawer-tab.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics/io/rattlesnake_specification.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{visualdynamics-0.1.0a1 → visualdynamics-0.1.0a2}/src/visualdynamics.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|