| Age | Commit message (Collapse) | Author |
|
Device tab stays hardware-only (pins, wiring, may vary per workstation);
the Channels window owns the profile-facing identity. Concretely:
- Fix a real regression: SerialDevice generic-format Apply & Reconnect
nulled out custom channel_names/units every time (no fallback, unlike
every other optional field in reconfigure()), silently reverting to
sig0/sig1/... ids.
- Plot legend now live-tracks the current Signal/Channel name instead of
freezing tr.label at add-time, so a rename shows up without re-adding
the trace.
- Add an inline quick-reconnect (combo + button, pre-selects the actual
current source) directly on each Channel block in the Channels window,
for the common single-source case — rewiring hardware no longer means
touching every Plot/pipeline that referenced it.
- Collapse the Channel ID field behind an "ID ▸" advanced toggle;
channel_id is already auto-derived from the name (_slug_id) and rarely
needs to be seen.
- Devices tab's Signal "Name" column relabeled "Label" with a tooltip —
de-emphasized now that Channel name is the real identity.
- Move "create a Channel from this Signal" from a column on the Devices
tab's signal table to a ⊕ button on each Signal row in the Channels
tab's Namespace panel — the action now lives where the identity is
created, not on the hardware tab. Only that instance opts in
(on_create_channel); the read-only Namespace dock on the main window
is unaffected.
- Floor the Namespace panel's width at its columns' natural size so
dragging the Channels-tab splitter narrower can't push the ⊕ column
behind the horizontal scrollbar.
- Breadcrumb log when a profile-saved Signal override can't be applied
because the signal_id no longer exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Apply & Reconnect rebuilt a device's whole Signal list from hardware
defaults on every call, silently discarding user/profile-set names,
units, and colors even when pins hadn't changed. Add
merge_signal_metadata() and call it from ArduinoDevice.remap_pins()
and SerialDevice.reconfigure() to reapply the same fields
ProfileManager already treats as user overrides, keyed by signal_id.
Also close a related gap: removing a pin that a Channel/Plot/Control
still referenced left it silently dangling, and SignalProcessor never
pruned its latest-value cache, so a derived Channel kept computing off
a permanently frozen stale reading. Add SignalProcessor.
on_device_reconfigured() to purge that cache, reference_registry.
find_references() for a console breadcrumb, and live "missing source"
indicators on Plot traces and Channel blocks reusing ControlWidget's
existing stale/QSS pattern. Also fixes ControlWidget._write() still
enqueuing writes to a channel that no longer exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Arduino analog Signal ids used to be the pin name itself, so fixing a
wrong pin silently changed the id and orphaned any plot trace, control,
or filter pipeline pointing at it. Analog Signal ids are now positional
(ai0, ai1, ...) and decoupled from the underlying pin, matching how
digital I/O already worked.
More broadly, formalizes a Device -> Signal -> Channel -> Plot naming
model (SignalConfig for physical device channels, Channel for derived/
computed ones) and adds core/reference_registry.py to propagate any
Signal or Channel id rename through plot traces, controls, and filter
pipelines instead of leaving them dangling. Signal ids are now editable
in the Signals tab with the same propagation.
.labui profile schema bumped to v2.0 with a transparent migration for
old files, including remapping old pin-name Signal ids to the new
positional ones. Removed six confirmed-dead files (two legacy device
classes, four superseded top-level ui/ duplicates).
|
|
Several compounding freeze sources in plugins/motion_capture/:
- tracker.py: _run_real had no frame-rate cap independent of the camera's
configured fps (up to 120fps/2560x1440 was possible), and no bound on
consecutive failed reads — a camera that "opens" (cv2 isOpened() can lie,
seen with Windows MSMF/obsensor) but never actually delivers frames would
get hammered forever. Added a 20Hz processing cap, a real test-read before
committing to a camera, and a give-up-after-30-failures fallback to
simulation.
- tracker.py/device.py: cv2.VideoCapture() opening a real camera is a slow,
blocking call — CameraTracker.start() used to do this synchronously on
the caller's thread, which froze the whole app when ProfileManager.apply()
called it during profile load. Now runs on the background thread; status
reporting (CameraDevice) moved to an on_result callback since the caller
can no longer learn the outcome synchronously.
- window.py: the Camera View preview ran a 30Hz GUI-thread timer doing a
full-frame copy + QImage/QPixmap rebuild every tick unconditionally (no
visibility check) — dropped to 10Hz and skips entirely when hidden.
Camera enumeration (list_cameras(), up to 8 sequential cv2.VideoCapture
probes) also ran inline in the window constructor, blocking it open the
same way camera_panel.py's CameraScanThread already avoided for the Add
Device dialog — moved to the same async pattern here.
config_window.py: separately, ConfigWindow._dev_poll (device status label
refresh) was stopped in closeEvent and never restarted, since MainWindow
reuses the same ConfigWindow instance across opens — invisible before
because connect() was synchronous, but with the above change a device can
sit in CONNECTING for a while, and a stale poll made that look stuck
forever after reopening the window. Restart it in showEvent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Windows/Linux self-register via core/file_association.py (winreg /
XDG .desktop+mime), triggered from a new Settings button since there's
no installer to do it at install time. macOS gets it at build time via
labui.spec's new BUNDLE() step (CFBundleDocumentTypes in Info.plist) —
that step also makes mac builds a real .app bundle for the first time,
not just a bare onedir folder. main.py gains a LabUIApplication
subclass to catch the macOS QFileOpenEvent (buffered if it arrives
before MainWindow exists) plus argv[1] handling for Windows/Linux.
ProfileButton.load_path() extracted so both paths share the same
load-and-apply logic as the existing file-dialog flow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Channel ID renames
Generalized the Plot pane drag-to-reorder (_PaneDragHandle/_PlotDropContainer
-> _DragHandle/_DragReorderContainer) and reused it for the Channels tab's
derived-channel list; reordering also updates SignalProcessor's evaluation
order (list order matters for chained Channels) via new reorder_derived().
Namespace panel: added an ID column (bare channel_id, matching the actual
channels.get(...) script convention) and fixed the Source column for
Channels to show resolved absolute references (dev_id/ch_id for a Signal,
bare id for a chained Channel) instead of the operation kind, so a chain
is traceable by eye.
Fixed a real bug this surfaced: renaming a Channel's ID never propagated
to other Channels' sources, leaving dangling ("derived", old_id)
references that silently stopped updating. Added
SignalProcessor.rename_derived() to fix up sources plus carry the ring
buffer/latest-value cache to the new key, wired to DerivedBlock's Apply.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Devices tab split into Physical Devices (left) / Signals (right); Channels
tab now hosts the Namespace browser (left) alongside Channels (right,
larger by default). Plot Builder panes gain drag-to-reorder (same mechanics
as ControlPanel's widget list, adapted for the editable pane title via a
dedicated grip icon), "+ Add Channel" moved beside Size for a more compact
row, and Min/Max/Size/line-width spin boxes widened so values no longer
overlap the up/down arrows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
- Split device tab into pysical devices and signals.
- Replaced old Signals panel (under Channels tab) with namespace.
to make Channel creation easier.
|
|
|
|
Devices: auto-name from OS-reported port description (Device Manager /
udev string) instead of a generic label; default Serial #Channels 1;
consistent simulate-mode checkbox across backends; inline device rename.
Channels: calibration/filters move out of Signals into a Channel
"Operation" (adds Scale/Offset alongside the existing math kinds);
single-physical-source Channels get an embedded Filters block; new
Channels auto-name/auto-ID from their source and reject duplicate
names/IDs on apply; "Create Channel" button on each Signal row; renamed
"Sources" -> "Signals" in the Channels UI; Signals' READ/WRITE dir badge
(was silently-broken IN/OUT) now reflects the real writable flag.
Plot: default layout is one pane per Channel (not per device, not raw
Signals) titled by channel name; "+ Add Channel" is a checkbox dialog
instead of an inline dropdown; traces get up/down reorder; live unit
changes (e.g. a Mark-10 gauge switching lb/kgF/N/ozF) propagate to an
open pane's axis label without clobbering a manually-typed one.
Control Panel: hardware assignment now picks from the same writable-
Signals list instead of a separate device/channel combo with a
hardcoded Arduino pin list; stale visual indicator when a control's
target device/channel disconnects or disappears; documented the `vars`
bridge (Control Panel On Action Script <-> Channel scripts) in both
directions' help text and script templates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
disappear and no sidebar panel.
Possible future implementation.
|
|
|
|
Debug button, DebugWindow, and the stdout/stderr tee (core/debug_log.py)
weren't wanted. developer_mode toggle stays — still gates device
Simulation Mode checkboxes and verbose terminal logging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
protocol updates
origin/main (23 commits) added a Debug window/log system on top of the old
separate Devices/Channels/Plot windows, plus protocol fixes (cml, mark10,
modbus_rtu, scpi) and device/profile changes. Local main (3 commits)
replaced the separate windows with a unified ConfigWindow + dock panel.
Kept local's ConfigWindow/dock architecture and ported the Debug window
onto it: new toolbar button + _open_debug(), gated by developer_mode same
as origin's version. Deduped the two independent "developer mode" toggles
that had collided in SettingsWindow (origin's General-tab checkbox gating
Debug window + device sim-mode visibility, local's Advanced-tab checkbox
setting verbose logging) into one General-tab checkbox that does both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Replaces three separate floating windows with single tabbed ConfigWindow:
- Tab 0: Devices (device cards, add/remove/configure)
- Tab 1: Channels (signal table with Dir/IN/OUT badge + pipeline ⚙ per row; derived channels pane)
- Tab 2: Plot (layout canvas + pane config)
Adds is_output field to ChannelConfig for output channel direction badges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Added dedicated firmware file for arduino
- Added View button to consolidate windows
- Control Panel changes:
- Changed name fron "Controls" to "Control Panel"
- Blue side bar when Panel is hidden.
- Supports "popping" out contorl panel
- Settings > Advanced tab added with developer settings
|
|
Branding:
- Rename app, window title, file extension (.labdaq → .labui), user data
dirs (~/.labui/), spec file (labdaq.spec → labui.spec), and APP_NAME
throughout all source, docs, and config files
Plugin system:
- Plugins no longer bundled in the PyInstaller build — installed at
runtime by users via Settings → Plugins → Install Plugin (zip)
- PluginManager now takes user_dir + extra_scan_dirs; user plugins live
in ~/.labui/plugins/, dev scan additionally covers project plugins/
- install_from_zip / uninstall / is_user_installed added to PluginManager
- vendor/ dir inside plugin zips: prepended to sys.path at load time so
plugins can ship their own deps without requiring pip on end-user machine
- source_url field in manifest.json: shown as Download button in the
missing-plugins dialog when a profile requires an absent plugin
- Frozen-app pip install now targets ~/.labui/plugin_packages/ using a
real system Python (sys.executable is the exe in frozen builds)
Profile loading:
- Profile now stores plugins_manifest snapshot (id, name, version,
source_url) alongside plugins_enabled
- On load, missing or dep-broken plugins trigger MissingPluginsDialog
before the rest of the profile is applied; user can install from zip
or download via source_url in-dialog, or cancel the load
- Plugin reconciliation only enables installed plugins — missing ones
are not written to enabled.json
UI:
- Version label added to status bar (bottom-right, muted colour)
- Settings → Plugins tab: Install Plugin… button, per-plugin Remove
button for user-installed plugins, live list refresh after install/remove
Docs:
- New docs/building.md covers the full release pipeline
- docs/plugin-development.md updated for new install flow, vendoring,
source_url, profile behaviour, and distribution instructions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Full pipeline, verified end-to-end against the real installed tufup
0.10.0 API (initial docs/summaries turned out inaccurate in places —
e.g. the apply method is download_and_apply_update, not update;
confirmed by inspecting installed package source directly rather than
trusting docs alone):
- core/version.py: single-source app version constant.
- labdaq.spec: PyInstaller onedir build (must be onedir, not onefile —
tufup replaces individual files in the install dir on update).
Bundles ui/*.qss, plugins/ (needed for runtime plugin discovery), and
repository/metadata/root.json once repo_init.py has produced one.
Built and smoke-tested: the frozen exe launches and stays running.
- scripts/release/{repo_init,repo_release}.py: maintainer-run release
tooling using tufup.repo.Repository, manual local signing (keys never
touch CI). Both actually run end-to-end during development of this
feature against a real build, not just written and assumed correct.
Longer expiration_days than tufup-example's CI-oriented defaults
(targets/snapshot/timestamp 90d instead of 7d/7d/1d), since we're
signing manually, not on an automated daily schedule — see
scripts/release/README.md for the re-signing cadence this still
requires even between releases.
- core/updater.py: thin Client wrapper. Refuses to run outside a
frozen build (getattr(sys, "frozen", False)) since there's no
installed bundle for tufup to update in `python main.py` dev mode.
Bootstraps the bundled root.json into the metadata cache dir on
first run — tuf.ngclient.Updater loads root.json from local disk on
construction, it does not fetch it remotely by design (the root of
trust can't come from the same server being verified).
- core/app_settings.py: factored out app_data_dir() (was inline in
_settings_path()) so the updater's metadata/target cache dirs live
in the same per-user location as settings.json, deliberately outside
the install directory an update can replace/move.
- Settings > General: version display + "Check for Updates" button,
manual-only per discussion (no silent background network calls or
surprise restarts for a lab-instrument-control app).
Metadata/targets are hosted on this repo's "updates" GitHub Release —
a fixed tag, not a normal per-version tag, because TUF's top-level
metadata needs a stable URL across app versions. No such GitHub-Releases-
hosting example exists in tufup or tufup-example; verified this by
fetching tufup-example's actual GitHub Actions workflow file directly
after a web search wrongly suggested one existed — the design here is
ours, not copied from upstream.
Not yet done, deliberately left for the user: running repo_init.py for
real (generates production signing keys), and creating the actual
"updates" GitHub Release. Both are irreversible-ish, security-sensitive,
externally-visible actions outside what should happen without the
user directly driving them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
|
|
|
|
|
|
New Settings > General > "Developer mode" checkbox (default on, so
this repo's simulate-by-default workflow is unaffected out of the box)
persisted through the existing core/app_settings.py load/save
functions. Exposed to code that can't easily receive the settings dict
via core.app_settings.is_developer_mode()/set_developer_mode(), a
small in-memory cache MainWindow keeps in sync whenever settings are
loaded or applied.
Debug window (ui/windows/debug_window.py): minimal first pass per the
plan — a live console. core/debug_log.py tees stdout/stderr into a
ring buffer + Qt signal (installed once in main.py, before anything
prints), so the window shows everything printed since app start,
including from background poll threads, not just what's printed while
it happens to be open. Its toolbar button is hidden unless developer
mode is on.
Simulate gating: every device's Simulation Mode checkbox/dropdown is
now hidden when developer mode is off — ui/add_device_dialog.py (also
covers the motion_capture camera panel, which shares this same
checkbox rather than having its own), and the per-device config
widgets in analog_input.py, arduino_device.py, digital_io.py,
nidaqmx_device.py, serial_device.py. Hiding rather than force-clearing
an already-simulating device's state — an existing simulated device
keeps working if developer mode is turned off later; the option is
just not offered again until it's back on.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
manifest.json already had an unused "requires" field (motion_capture's
manifest lists opencv-python>=4.8.0) — PluginManifest never parsed it,
so a missing dependency just crashed the import inside _load_plugin(),
caught by the broad except and only ever printed to the console.
Adds PluginManifest.requires, PluginManager.missing_requirements()/
get_missing_dependencies(), and an early check in _load_plugin() that
skips the risky import entirely when a requirement is missing.
main_window.plugin_enable() now checks this before calling
PluginManager.enable() and shows a QMessageBox with the missing
packages and a pip install command instead of failing silently.
Checks by distribution name via importlib.metadata (what pip installed
it as), not import name — those differ for packages like opencv-python
(imports as cv2) or pyserial (imports as serial), so importlib.util.
find_spec() would give false negatives.
Also fixes a button-state bug this surfaces: SettingsWindow's plugin
toggle optimistically flipped to "Disable" the instant Enable was
clicked, before knowing whether enabling actually succeeds — pre-existing,
but now trivially reproducible (any plugin missing a dependency). Enable
no longer flips the button immediately; main_window calls the new
sync_plugin_button() once the real outcome is known.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Relabel every channel picker/header to "NAME (DEVICE/SIGNAL)" instead of
the previous "DEVICE/SIGNAL (NAME)" ordering, consistently across
channels_window.py (_channel_combo, ChannelPickerDialog,
ChannelPipelineBlock header), plot_builder.py, plot_config.py,
control_editor.py, and plot_window.py.
While touching each of those pickers, added the missing `.enabled`
filter that _make_picker()/_x_cb/_build_channel_picker lacked (the
default-layout builders already filtered disabled channels; these
manual "add channel to pane" pickers didn't).
Devices window Signals tab: removed the separate "Signal ID" column
(folded into the existing non-editable Device column instead of the
editable Name column, to avoid corrupting the in-place channel rename
feature that column already supports).
Channels window Virtual Channels pane: added a "Source" dropdown next
to "+ Add Channel" — picking a source pre-seeds the new derived
channel with it via DerivedBlock._add_src(); leaving it on the default
"none" entry keeps today's behavior of creating an empty channel.
Note: docs/ToDo.md's "Channels window UI" items referred to the *live*
UI (this window's PipelineTab + the Devices window's Signals tab), not
SignalsListTab in this same file, which its own module docstring flags
as dead/unused code kept only for old-profile compatibility — verified
by grepping for any instantiation of it (none exist).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
DeviceInfo.name existed but was hardcoded per device type and never
operator-editable. Adds a Display Name field to Add Device and makes
the Info-tab Name field in the device config dialog editable, and
threads the name through get_save_config()/ProfileManager.apply() so
a custom name survives a .labdaq save/reload instead of reverting to
the type default. Name is applied post-construction rather than as a
constructor kwarg, since no device factory declares a "name" param.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
None op
- Variable names derived from signal display name (e.g. "Acceleration X" → acceleration_x)
instead of raw channel ID; all auto-names lowercase with underscores
- Template code auto-updates x[0]/x[1] refs to named vars as sources are added/changed;
stops updating once user edits the code manually
- Derivative wrt-channel mode uses separate template with x[1] substituted to channel var
- Added "None" passthrough operation (result = x[0])
- source_names persisted in .labdaq profiles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- custom_script now uses exec-style (assign to `result`) matching the
built-in templates — previously auto-wrapped in def compute() which
had no return statement, causing state and channel vars to silently fail
- function kind unchanged: auto-wraps body as def compute(x,t); use return
- Add SCRIPT_TEMPLATES for expression, function, custom_script kinds;
shown automatically when user switches to a script kind (replacing stale
code from previous kind)
- New channels default to template when no saved code exists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Variable names now appear as a live '# Variables: A0 = x[0] · ...'
comment at the top of the code editor (function, custom_script, and
built-in read-only templates); updates on every source add/remove/change
- Derivative 'With Respect To' channel combo now included in variable list
as x[1] when channel mode is selected
- _apply() strips the auto-generated comment before saving the script so
it doesn't pollute the stored expression
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When sources are added, removed, or changed in the derived channel editor,
the code group now shows a live "Variables: A0 = x[0] · encoder = x[1]"
hint above the code editor so users know the exact names to use in scripts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Named variables in expressions: source channel IDs injected as Python
identifiers alongside x[] (e.g. write `A0 * 2` instead of `x[0] * 2`)
- Shared `vars` dict on SignalProcessor accessible from all expressions,
functions, and control scripts; persisted in .labdaq profiles
- Per-derived-channel `state` dict for persistent computation state in
custom scripts
- Control widgets gain optional on_action_script (Python snippet with
value, vars, channels, math in scope); syntax-checked in editor dialog
- Built-in derived kinds (derivative, rms, power, etc.) now show their
Python implementation as read-only code previews; "Edit as Custom Script"
converts to custom_script kind with template pre-filled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Naming conventions:
- Devices window: "Channel ID" column → "Signal ID"
- Serial device auto-gen IDs: CH# → sig# (generic, SCPI, Modbus fallbacks)
- Channels window physical section: "Physical Channels" → "Signals"
- Channels window virtual section: "Virtual Channels" → "Channels"
- "Add Virtual" button → "Add Channel"
- New derived channel default name: "Virtual Channel #" → "Channel #"
Signals panel (ChannelPipelineBlock):
- Remove Calibrate header button; add inline Calibration section in body
- Remove Math section (Derivative/Integral) entirely
- scale_offset excluded from filter dropdown; routed to Calibration section
- Existing pipelines: scale_offset loads into Calibration, math filters dropped
- _FILTER_KEYS excludes derivative, integral, scale_offset
Splitter default position:
- Left (Signals) pane initialised to minimum content width via QTimer.singleShot
- setStretchFactor(0,0) / (1,1): left stays narrow, right grows on resize
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
Split zone was unreachable: squeezed between outer squeeze (1/6) and
center (1/3), leaving a 1/6-wide band impossible to target reliably.
Users hitting the tile edge always triggered squeeze instead of split.
- Remove squeeze from inside-tile detection entirely. Split now owns
the outer 1/3 of each tile edge — large, easy to target. Center
keeps the inner 1/3. Squeeze remains gap-detection only (second pass).
- Fix canvas-edge squeeze (no adjacent neighbor): instead of bisecting
the edge tile, wrap the entire remaining tree in a new directional
split with the dragged tile placed at the outer position, then
equalize — all tiles end up equal size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Fix squeeze zone pop-out: _adjacent_tile used self._drag_tile (already
None at drop time) to exclude the dragged tile from neighbor candidates.
Pass dragged_idx explicitly via exclude_idx param instead.
- Swap split/squeeze detection areas: outer 1/6 now triggers squeeze
(insert-between), inner 1/6→1/3 band triggers split (bisect target).
- Extend squeeze detection to gaps between tiles and canvas edges via a
second pass in _zone_at that expands tile rects by _LC_GAP+2 and finds
the closest outside edge. Canvas-edge squeeze falls through to bisect.
- Dynamic canvas height: sync_panes sets height to
max(160, n*(min_tile+gap)+2*gap) so 4+ panes never get crushed below
minimum tile size.
- Remove _tree_grid_size cap of 12: LCM was truncated prematurely,
causing unequal rowspans in the strip chart for 5+ panes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Root cause: PlotWindow deepcopied _chart._cfg on init, so refresh_channels()
operated on a stale copy. Also, when _win_plot was None, _on_derived_changed
did nothing to prepare the config for when it opened.
Fix:
- refresh_channels() now owns the derived-pane sync: adds panes for new
virtual channels (updating BSP tree), removes panes for deleted ones
- _open_plot always passes current _chart._cfg reference then calls
refresh_channels() so derived panes sync on every open
- _on_derived_changed syncs cfg reference before calling refresh_channels()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
|
|
_zone_at used absolute pixel distance to pick dominant axis, which always
favoured the short (vertical) axis for landscape tiles. Replaced with
normalized fraction (dist/dimension), so left/right zones are accessible
on wide tiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Zone layout (outer to inner per edge):
0–1/6 squeeze (amber): insert dropped tile between two existing tiles
1/6–1/3 split (blue): bisect the target tile 50/50
1/3–2/3 center (green): swap/move
Squeeze behaviour: find the tile adjacent to the target in the drop
direction; insert dragged tile to the opposite side of that neighbor
so it lands visually between them. Falls back to split when no neighbor
exists (edge tile).
- _zone_at: returns "squeeze_*"/"split_*"/"center" with dominant-axis
selection (nearest edge wins)
- _adjacent_tile: finds geometrically adjacent tile by pixel proximity
- _do_drop: routes squeeze→insert-at-neighbor, split→bisect, center→swap
- paintEvent: amber/blue/green per zone type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Edge zones (left/right/top/bottom): split target tile equally
- Center zone: swap dragged and target tile positions in BSP tree
(tree structure preserved, only leaf pane values exchanged)
- Center overlay: green highlight (vs blue for split zones)
- _tree_swap: immutable swap of two leaf pane values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
After each drag-drop, add, or remove, walk the tree and reset each split's
ratio to left_leaves/(left_leaves+right_leaves). This distributes space
equally per pane regardless of the prior tree structure.
- _tree_equalize_ratios: recomputes all ratios bottom-up by leaf count
- _tree_leaf_count: counts leaves in a subtree
- _tree_grid_size: uses LCM of split denominators (capped at 12) instead
of 2^depth, giving exact integer rowspans for equal distributions
- _tree_default: balanced binary tree + equalize (not uneven chain)
- Applied in _do_drop, _add_pane, _rm
Result: 3 side-by-side panes → equal widths; any mixed layout → equal area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Dragging a tile to the edge (left/right/top/bottom) of another tile
splits that tile's space and inserts the dragged tile on that side.
Drop zones occupy the outer third of each tile edge; center has no zone.
Ghost overlay shows the split target during drag.
Data model: LayoutConfig.tree (BSP tree as nested dict). PaneSpec.row/col
removed. strip_chart uses tree_to_grid() → rowspan/colspan for pyqtgraph.
Backward compat: tree=None falls back to layout_mode stacking path.
Example: 3 stacked → drag Tile 3 to left of Tile 1 → Tile 3|Tile 1 on
top (side-by-side), Tile 2 spans full bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Replace layout radio buttons (Stacked/Side-by-Side/Grid) with a visual
LayoutCanvas widget. Pane tiles snap to grid cells on drag-drop; dropping
on an occupied cell swaps positions. Ghost highlight shows snap target
during drag.
- PaneSpec gains row/col fields (None = unassigned; backward compat preserved)
- LayoutConfig gains "free" mode; strip_chart reads explicit positions
- PaneBlock loses ▲▼ move buttons; reordering is canvas-only
- QSS entries added for layoutTile, layoutTileDragging, layoutCanvasBar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Remove Qt.WindowType.Tool flag — it suppresses the maximize button.
Window now has standard chrome with minimize/maximize/close.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
virtual blocks
Physical channels panel: horizontal splitter (left/right), channel picker
dialog replaces dropdown, Filters and Math sections per block, Calibrate
button, pipeline-enabled checkbox removed. Virtual channels: collapsible
blocks, enable checkbox removed, kind dropdown removed (defaults to
expression). Window resized to 1400×820.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- New core/app_settings.py: load/save settings.json to platform config dir
(Windows: %APPDATA%, Linux: ~/.config, macOS: ~/Library/Application Support)
- MainWindow loads saved settings on startup, saves on every Apply & Close
- Settings survive profile switches and app restarts independently of profiles
- Settings > General: Reset to Defaults button with QMessageBox confirmation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Always insert one source row for derivative/second_derivative on build
- Guard in _on_kind: add row when switching to derivative with no sources
- Code editor: monospace font (IBM Plex Mono/Consolas fallback)
- Code editor: tab stop = 4 character widths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Reflects current design: split Physical/Virtual Channels layout,
no tabs, no legacy Signals list as primary UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Default ID: sig_N → CH_VN
- Default name: Signal N → Virtual Channel N
- Rename "Kind" label to "Operation"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Split PipelineTab into Physical/Virtual sections via QSplitter
- Each section has independent scroll area, header, and add controls
- Add "With Respect To" control for derivative/second_derivative virtual channels: Time (dy/dt) or Channel (dy/dx)
- When Channel selected, x variable picker appears for any physical or derived channel
- signal_processor computes dy/dx using channel inputs when deriv_wrt=channel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|