summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-07-29Merge branch 'fix/run-stop-log-master-switch'Christian Kolset
2026-07-29Merge branch 'feat/plugin-pip-install-prompt'Christian Kolset
2026-07-29Merge branch 'feat/mark10-unit-autodetect'Christian Kolset
2026-07-29Merge branch 'feat/device-naming'Christian Kolset
2026-07-29Merge branch 'feat/dev-debug-window'Christian Kolset
2026-07-29Add Developer Mode setting, Debug window, and gate simulate behind itChristian Kolset
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>
2026-07-29Offer to pip install missing plugin dependencies instead of just warningChristian Kolset
Builds on the previous branch's dependency check: the missing-deps QMessageBox is now a Yes/No prompt. Yes runs a blocking `sys.executable -m pip install <reqs>` (subprocess.run, output captured), shows a result dialog, and on success proceeds straight to enabling the plugin — no need to click Enable a second time. Blocking is a deliberate simplification, not an oversight: this codebase has no worker-thread/progress-dialog pattern for slow operations anywhere else, so a threaded installer would be inconsistent with everything else here. pip install is a one-time, infrequent action, unlike e.g. a serial connect that runs constantly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29Check plugin dependencies before loading, warn instead of silent console errorChristian Kolset
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>
2026-07-29Make Run the master stop switch; add Log button disabled/recording stylingChristian Kolset
Run/Stop previously only paused acquisition — control outputs (motor speed, switches, PWM duty) kept whatever value was last written, so stopping the run loop didn't stop a running motor. Adds ControlWidget.safe_stop() (per-widget override, default zeros the output) and ControlPanel.safe_stop_all(), called from _toggle_run's Stop branch before engine.stop(). Per-widget behavior is deliberately not uniform: - OnOffSwitch/MotorControl/PwmControl have a latched running/enabled state, so safe_stop() drives their own toggle handler (consistent UI + write in one path) and, for Motor/PWM, zeroes the slider too. - SetpointControl/AnalogOutputControl only write on an explicit user action and have no universally safe forced value (e.g. 0 isn't necessarily "off" for an arbitrary process setpoint or analog output) — Stop leaves them untouched rather than guessing. Log button: added a :disabled QSS rule so "can't log yet" reads as clearly inert rather than a duller version of the enabled look, and a 600ms blink (toggling a "recording" dynamic property the QSS keys off) while a recording is active, so it reads as live/recording rather than a static pressed button. Master Stop now calls _toggle_log(False) explicitly when forcing the button off, since QPushButton.setChecked() doesn't emit clicked — without this the blink would keep running after a master Stop even though logging itself already halted via engine.stop()'s internal stop_logging() call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29Sync Mark-10 gauge's live unit into ChannelConfig instead of a fixed defaultChristian Kolset
Mark10Layer._parse() already tracked whichever unit suffix the gauge last reported (lb/kgF/N/ozF — the gauge's physical unit button cycles these independently of this app). serial_device.py hardcoded the "force" channel's unit to "N" and never updated it, so switching units on the gauge itself was invisible here. Adds Mark10Layer.current_unit and has SerialDevice.read_channels() keep the "force" ChannelConfig's unit in sync with it on every poll. This fixes the unit shown in anything that reads ChannelConfig.unit live — new plots, channel pickers, CSV log headers — but does not relabel the Y-axis of an already-open plot pane, since plot axis labels are baked into PlotConfig.y_label once at plot-build time, not re-read from the channel live. Making an open pane's axis relabel itself would need new signal plumbing from the device through AcquisitionEngine to the strip chart — a bigger, separate change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29Rework channel labeling and add source picker to virtual-channel creationChristian Kolset
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>
2026-07-29Add editable device display name, persist it across profile save/loadChristian Kolset
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>
2026-07-28Merge remote-tracking branch 'refs/remotes/origin/main'Christian Kolset
2026-07-28Update CML protocol documentation and improve command handling; adjust ↵Christian Kolset
motion_capture setting to false
2026-07-28Added documentation for CM1 servo motorsChristian Kolset
2026-07-27Enhance protocol layers: implement I/O locking for thread safety in serial ↵Christian Kolset
communication
2026-07-27Fix .gitignore: remove duplicate entry for plugins/enabled.jsonChristian Kolset
2026-07-27Enhance SerialDevice: support write-only channels and improve raw data handlingChristian Kolset
2026-07-27Add simulation handling to protocol layers and update motion_capture settingChristian Kolset
2026-07-14ignoring .venv directoryChristian Kolset
2026-06-09Derived channels: signal display names as vars, auto-update template code, ↵Christian Kolset
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>
2026-06-09Fix custom_script execution + add templates for expression/function kindsChristian Kolset
- 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>
2026-06-09Channels: inject variable names as comment in code editor, fix derivative wrtChristian Kolset
- 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>
2026-06-09Channels: live variable-name hint in code editor updates with sourcesChristian Kolset
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>
2026-06-09Scripting: named signal vars, shared vars dict, control scripts, code templatesChristian Kolset
- 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>
2026-06-09Channels/Signals UX: rename labels, calibration section, splitter, sig# IDsChristian Kolset
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>
2026-06-09UI: streamline Add Device dialog and serial config widgetChristian Kolset
Add Device dialog: - Replace Available Devices QGroupBox with plain devWindowTitle label to match Configuration section style — no box/border - Auto-scan on open via QTimer.singleShot(0) - Rename Scan All → Refresh Serial config widget: - Fix gap between protocol panel and Available Serial Ports: introduce _AdaptiveStack (QStackedWidget subclass) that reports only the current panel's sizeHint; set Fixed vertical size policy so the stack never expands beyond its content regardless of window size - Stretch added between Available Serial Ports and Apply & Reconnect so the button stays pinned to the bottom - Rename Channels → Signals in _GenericPanel and _SCPIPanel (Signal Queries) Config dialog (device info tab): - Rename Channels → Signals Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09Motion capture: profile support, FPS config, persistent tracking, window stateChristian Kolset
Profile integration: - CameraDevice.get_save_config() serialises camera_index, simulate, resolution, fps — cameras now persist in .labdaq profiles - ProfileManager.register/unregister_device_factory() lets plugins register their device classes for profile restoration without core → plugin dependency - MotionCapturePlugin registers CameraDevice factory on load/unload Camera configuration: - Add Frame Rate combo to CameraPanel (Default/15/24/30/60/120 fps) defaults to 30 fps; flows through to CAP_PROP_FPS on connect - tracker.start() accepts fps param alongside existing resolution Tracking persistence: - CameraTracker.is_running() — checks thread alive state - MotionCaptureWindow initialises _connected from tracker.is_running() so reopening the window resumes the live feed without restarting the camera or losing the active tracking state Window state: - Plugin saves px_per_mm + camera_idx when window closes - Reopened window restores saved values and syncs mode/shape UI from tracker state via restore_ui_state() (signals blocked to avoid resetting tracker) - Window type changed to Qt.WindowType.Window for independent close button on all Linux WMs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09Merge fix/motion-capture: template matching + frame-freeze selection UIChristian Kolset
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09Upgrade motion capture: template matching, frame-freeze selection, resolutionChristian Kolset
Tracking: - Add "template" mode (default) using snapshot + shape-masked matchTemplate - Add "csrt" mode as alternative for texture-rich targets - set_roi_rect(x, y, w, h, frame_rgb) initialises from exact selection rect - set_roi() delegates to set_roi_rect (centred square, backwards-compat) - start() accepts resolution=(w, h) to set camera capture resolution Selection UX: - Replace QLabel feed with FrameSelector widget (custom QWidget) - "Click to Track" freezes frame; user scrolls to zoom (anchored to cursor), right-drags to pan, left-drags to draw rubber-band selection - Shape preview: rectangle or inscribed ellipse depending on mode - "Confirm Selection" extracts template from frozen frame and starts tracking - "Clear" cancels selection or clears active tracking Configuration: - CameraPanel adds Resolution combo (Default / 640×480 / 720p / 1080p / 1440p) - CameraDevice stores and forwards resolution to tracker.start() - Template Match listed first in Mode combo; shape combo hidden for CSRT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09Merge fix/add-devices-steamline: streamlined Add Device dialogChristian Kolset
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09Redesign Add Device dialog with streamlined two-section layoutChristian Kolset
- Unified device scanner at top (serial ports, NI devices, cameras) replaces per-panel scan groups; clicking pre-fills port/type - Port, baud rate, NI Device, and simulation toggle moved into Available Devices section; shown/hidden by device type - Device type, ID, and Protocol/Format grouped under Configuration - Panel order: Serial/UART → Arduino → NI-DAQ (Serial first) - Add button always enabled; validation on press shows error popup with focus jump to the offending field - CameraPanel loses sim_chk (sim state set by dialog via set_simulate) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04docs: update CLAUDE.md — add plugin system, BSP tree layout, opencv depChristian Kolset
2026-06-04Merge plot-builder-layout-feature: layout canvas drag-drop fixesChristian Kolset
2026-06-04Restore Motion Capture toolbar button with multi-camera supportChristian Kolset
get_toolbar_actions() returns a checkable 🎥 button. On click: - 0 camera devices → info message, button unchecks - 1 camera device → opens MotionCaptureWindow directly - N camera devices → QInputDialog picker, opens selected device's window Windows tracked per device_id; closing a window removes it from the map and unchecks the button when none remain. on_unload closes all open windows before deregistering the Camera panel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04Redesign motion capture plugin as Camera device typeChristian Kolset
Plugin now registers a "Camera" option in the Add Device dialog on load (removes it on unload) instead of auto-creating a single device. - camera_panel.py (new): CameraPanel with CameraScanThread that probes OpenCV indices 0–5, lists found cameras with resolution, shows "No cameras detected" when none available, simulation checkbox. build_device() returns CameraDevice for the selected index. - device.py: rewrite MotionCaptureDevice → CameraDevice. Each instance owns its own CameraTracker created in connect(). Status is CONNECTED or SIMULATED based on tracker.simulated. get_config_widget() shows source/status and an "Open Camera View" button that launches the existing MotionCaptureWindow for that device's tracker. Backward-compat alias MotionCaptureDevice = CameraDevice kept for saved profiles. - plugin.py: simplified to on_load/_PANELS registration + pixel_to_mm filter. No auto-devices, no toolbar button, no settings widget. Camera data flows through the standard Device → Signal → Channel → Plot pipeline like any other device. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04Fix split zone detection and canvas-edge squeeze behaviorChristian Kolset
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>
2026-06-04Fix layout canvas drag-drop: zone logic, pop-out bug, canvas heightChristian Kolset
- 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>
2026-06-03Fix virtual channels not appearing in Plot BuilderChristian Kolset
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>
2026-06-03Remove __pycache__ from trackingChristian Kolset
2026-06-03Changed dialog text window from channels to signals for clarityChristian Kolset
2026-06-03Merge plot-builder-layout-feature: dynamic BSP tiling layout canvasChristian Kolset
Replaces layout radio buttons (Stacked/Side-by-Side/Grid) with a drag-and-drop tiling canvas. Three drop zones per tile edge: - squeeze (outer 1/6, amber): insert between two tiles - split (1/6–1/3, blue): bisect tile 50/50 - center (green): swap tile positions BSP tree stores layout; tree_to_grid() converts to rowspan/colspan for pyqtgraph. _tree_equalize_ratios ensures all tiles share space equally after any operation. Backward-compatible with existing .labdaq profiles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03Fix horizontal zone detection for wide/short tilesChristian Kolset
_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>
2026-06-03Three-zone drop system: squeeze, split, centerChristian Kolset
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>
2026-06-03Add center-zone move (swap); restore center drop zoneChristian Kolset
- 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>
2026-06-03Equalize tile sizes after every BSP tree operationChristian Kolset
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>
2026-06-03Replace grid-snap canvas with BSP-tree drag-to-split tilingChristian Kolset
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>
2026-06-03Add drag-and-snap tiling layout canvas to Plot BuilderChristian Kolset
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>
2026-06-03Make Devices, Channels, Plot windows maximizableChristian Kolset
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>
2026-06-03Redesign Channels window: side-by-side layout, picker dialog, collapsible ↵Christian Kolset
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>