Connect matplotlib event handlers on the plot canvas so the display
can be navigated without engaging the toolbar modes: left button
drag pans the view, the mouse wheel zooms both axes around the
pointer, and right button drag zooms single axes (horizontal motion
zooms x, vertical motion zooms y, like the right button in the
toolbar pan mode). Linear (including inverted ppm) and logarithmic
axes are handled, the latter in log space. While a toolbar pan/zoom
mode is active the handlers stand back so the toolbar behavior is
unchanged. Documented in doc/manual.rst.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Two new buttons below the plot copy the current figure to the
clipboard. The PNG copy renders off-screen on a temporary Agg canvas
at a fixed 12:8 inches and 800 px width, independent of the window
size, and restores figure canvas, size and dpi afterwards so the
on-screen plot is untouched. The RGBA buffer is wrapped in GLib.Bytes
for GdkPixbuf.Pixbuf.new_from_bytes, which current PyGObject requires
(fixes the 'Expected GLib.Bytes, but got bytes' error). The SVG copy
keeps using savefig into a text buffer; both report through logger.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
save_display_data_as_text printed via the removed log() helper;
use logger.warning instead so the message lands in the log window.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
The middle-click context menu (crosshair toggle, copy as PNG/SVG)
duplicated functionality that is now reachable elsewhere: the
crosshair is toggled by its own toolbar button and the PNG/SVG
clipboard copies get dedicated toolbar buttons. Removing it frees the
middle mouse button on the plot canvas.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
quit_event ran its cleanup path again when a second delete-event
arrived after the state had already switched to Quit_State, hanging
the application on shutdown. Return early instead.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
The stock GTK3Agg canvas renders one buffer pixel per physical pixel,
so on a scaled (HiDPI) screen every full redraw rasterizes four times
the pixel count, which makes the interactive plot sluggish. Override
_update_device_pixel_ratio in BlitSafeCanvas to pin the device pixel
ratio at 1: the plot renders at logical resolution and the window
system scales the output up. Measured ~2.3x faster full redraws on a
2x-scaled screen; non-scaled screens are unaffected.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
The previous blit path converted the whole axes bbox (several megapixels
on HiDPI displays) from RGBA to premultiplied ARGB on every motion
event, costing about 84 ms per frame on a maximized window. Per frame
now only the thin strips around the old and new line positions are
erased from a cached full-figure background, redrawn and blitted,
which brings the frame cost below 1 ms.
- BlitSafeCanvas paints blitted regions immediately instead of queueing
them, which kept regular paints partial and wiped the axis labels
- CrosshairCursor coalesces motion events to 60 fps and erases strips
by direct numpy copy from the background; matplotlib's restore_region
with a bbox argument addresses a different coordinate frame and
restores the wrong area, so it is avoided
- after a full render the old line positions are blitted right away,
otherwise motion events processed before the pending repaint drew the
cursor at its new position next to the stale one on screen
- clear_display recreates the crosshair so it survives plot switches
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
The crosshair toggle button loaded the "crosshairs" icon from the
icon theme inside a bare try/except, so on systems without that
icon the button was empty and invisible. Replace it with a
self-contained red reticle (circle, center dot, cross arms) drawn
from inline XPM data, and set always-show-image so the desktop
default gtk-button-images=False cannot hide it.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- debian/rules: build sphinx HTML into build/html
- debian/install: install it as usr/share/doc/python3-damaris/html/
instead of shipping the raw doc/ source tree (whose gitignored
doc/_build never existed on CI checkouts)
- debian/control: declare ${sphinxdoc:Depends} and python3-sphinx
(provides sphinx-build) explicitly
- requirements.debian: install python3-sphinx explicitly
- DamarisGUI: point the manual at the packaged HTML path with a
source-tree fallback, populate the Help dropdown menu, and open
the DAMARIS manual on a plain toolbar click
- doc/manual.rst: document the Help menu
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
FitResult is a new Drawable in damaris.data with x-values independent of
the measured points, so fits can be sampled on a dense grid. It is stored
as its own data pool entry (e.g. "measurement/fit"), written to and read
from the HDF5 data pool (damaris_type "FitResult", params as attributes),
and drawn by the GUI as overlay on its measurement, including a fit
parameter text box. Fits get no entry in the display tree, so measurement
rows stay directly selectable. Result scripts create them via
FitResult(x, y, title, source, params).
Additionally, HDF5 files that experiment or result scripts left open
(e.g. after a script failure) are closed when the experiment stops.
pytables keeps such handles registered process-wide, which blocked
reopening the dump file and the scripts' own files on the next run.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Establish one invariant across the Drawable hierarchy: x is always a
numpy ndarray, y is always a list of per-channel ndarrays, and empty
data is the same types with zero length.
- Drawable: x starts as empty ndarray; min/max helpers return 0 on
empty data instead of raising; drop contradictory isinstance(self.y,
list) branches
- ADC_Result/Accumulation: empty constructors build ndarray x; full
constructors coerce x/y/y_square with numpy.asarray; Accumulation
get_ydata/get_yerr return empty ndarrays and no longer crash on
empty statistics-less accumulations
- MeasurementResult: fix get_lineplotdata to return the accumulated
means (previously the never-populated self.y, so the GUI line plot
could never draw); remove dead float/int branches in CSV and HDF
writers
- Error_Result: get_xdata/get_ydata return ndarrays
Add unittest coverage for the invariant, empty-state accessors,
constructor coercion, line plot data, CSV output and HDF round-trip.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Removing gobject.type_register(GtkSource.View) broke startup: the
call was load-bearing, it forced registration of the GtkSourceView
GType so GtkBuilder could resolve the type in damaris3.glade
("Invalid object type 'GtkSourceView'"). Restore the registration
by referencing gtksourceview.View at import time, which is the
same effect without the PyGTK compat API.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
time_period is list[datetime] | None at runtime (None from old-format
HDF5 files, [] from the constructor), but several sites subscripted
it unguarded and crashed with TypeError/IndexError.
- annotate time_period as list[datetime.datetime] | None
- copy paths (get_accu_by_index, __iadd__ self-empty) use the guarded
pattern already present in _copy_provenance
- merges in __add__/__iadd__ only combine min/max when both sides carry
time information; otherwise the result is None (provenance unknown)
- write_to_tecmag raises ValueError when no time information is
available instead of crashing while writing the header
Clears all remaining time_period pyrefly errors (72 -> 63). Unit tests
reproduce the crashes and pass with the fix.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Accumulation.__neg__ passed y_2 = numpy.array(self.y_square), storing a
2D ndarray. A later __iadd__ on an accumulation with ndarray y_square
crashed with AttributeError at the y_square.append sites (992, 1064).
- __neg__ now passes a per-channel copy (values unchanged: sums of
squares are invariant under negation)
- __init__ normalizes ndarray y_2 to a list, so external callers cannot
reintroduce the bug
- y_square annotated as _YChannels
Clears the remaining pyrefly errors at Accumulation.py:992 and :1064
(74 -> 72). Unit tests reproduce both crashes and pass with the fix.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- signature-only type hints for ADC_Result, MeasurementResult,
Accumulation, and the Drawable base class (x: ndarray | list, y: list)
- get_ydata now takes channel: int = 0 across the hierarchy
(Drawable, Accumulation, MeasurementResult, Error_Result), fixing
the LSP violation flagged by pyrefly
- widen _XArray alias to ndarray | list: internal copy constructors
pass self.x[:] which is that union
- add hinted files to pyrefly project-includes
pyrefly errors in the covered files drop from 102 to 74; the rest is
PyTables stub noise and pre-existing latent bugs left for separate fixes.
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- raise instead of silent None for unsupported operands and empty negation
- validate before acquiring lock in Accumulation operators (no RLock leak)
- Accumulation._copy_provenance helper, ADC_Result._copy_metadata for all result paths
- shared _sandbox_stdout_redirect contextmanager for sandboxed scripts
- TemperatureMonitor dead code removal (gtk helper, traceback, unused properties)
- new unit tests incl. lock-freedom regression test
- gitignore sphinx generated doc/_autosummary/ and doc/damaris/
phase() overwrote the underlying channel arrays (y[i] *= -1) and returned
self, so rotated copies aliased the original object and negative phase
angles garbled shared data. It now returns a copy with new arrays and
uses numpy.deg2rad for arbitrary angles.
Add unittests for phase() including negative angles and non-mutation.