Skip to content

visualdynamics.gui.panes

panes

Self-contained view panes.

A pane owns a view, the bar of controls that belongs to it, and the state those controls carry — and nothing else. It never reaches for the project, the tree or the selection: it reports that a choice moved and lets whoever placed it decide what that means. That is what lets the same pane sit in the main window, in a dock, or in a window of its own opened from a script.

Classes:

Name Description
DataPane

The 2-D data view: a plot under a bar saying how to read it.

ScenePane

The 3-D view: the scene, its camera, and how the scene is annotated.

Functions:

Name Description
offer_acts

Put the acts a selection can take on a bar, in their own fenced

Classes

DataPane

DataPane(
    theme_name: str,
    offscreen: bool = False,
    parent: QWidget | None = None,
)

Bases: QWidget

The 2-D data view: a plot under a bar saying how to read it.

The bar speaks three vocabularies at once — how coherence reads, which records an FRF is filtered to, and how an FRF beside a shape set is used — and shows only what the data in front of it can actually use, because a control that is not a live option is absent rather than greyed.

The pane keeps the choice and announces it. What a choice means stays outside: filtering to the diagonal moves the tree selection, and choosing Edit Fit opens a fitting session, neither of which is a plot's business.

Methods:

Name Description
show_acts

The acts the selection can take, on this bar (offer_acts).

offer_rms

Show the RMS toggle for a specification on its own.

show_spectra_views

Offer the overlay/ratio pair, or take it away.

show_comparison_views

Offer the three readings, or none of them.

show_scaling

Show the comparison scaling, or take it away with None.

show_srs_views

Offer the two readings of a shock-spectrum comparison.

show_replication_views

Offer the two readings of a replication, or neither.

show_events

Offer these repeats, keeping the one already chosen.

chosen_event

The repeat being looked at, or None.

step_event

Move the event box by step, stopping at either end.

reset_plot_mode

Back to letting the data choose. Called when the selection

offer_waterfall

Show the 2D/3D toggle, raised by the plain-curves path

offer_frequency_axis

Show the decades/hertz toggle for data drawn over frequency,

show_quantities

Offer a mixed object's quantity groups, keeping the choice.

show_pages

Offer the page stepper, or put it away for a single page.

chosen_page

The page on the stage, zero-based.

chosen_quantity

The quantity box's (dimension, hint) key, or None.

create_waterfall_plotter

Build the 3-D data surface, on first use.

reserve_bottom

Keep the bottom pixels of the plot area empty.

show_waterfall

Swap the plot surface: the 2-D graphics or the 3-D view.

component

Which part of a complex ordinate to draw.

show_pairs

Offer these comparisons, keeping the one already chosen.

chosen_pair

The comparison being looked at, or None.

select_pair_label

Show the comparison this label names. True if it moved.

step_pair

Move to the next comparison, or the previous one.

sync_toolbar

The bar shows exactly when something on it does.

show_controls

Show exactly the controls this data can use, and hide the bar

reset_controls

Nothing applies until something says otherwise.

show_pair_controls

The bar as the fitting screen wants it: the pair choice alone.

Attributes:

Name Type Description
showing_wavelet bool

Whether the scalogram is up — asked by whoever draws.

showing_kurtosis bool

Whether the bar reading is up — asked by whoever draws.

rms_wanted bool

Whether the RMS reading of a specification is asked for —

showing_averaging bool

Is the averaging view asked for? Only meaningful for a time

showing_filter bool

Is the filter view asked for? Only meaningful for a time

showing_octave bool

Is the octave-band view asked for? Only meaningful for a

showing_truncate bool

Is the truncate view asked for? Only meaningful for a time

showing_shocks bool

Is the shock view asked for? Only meaningful for a time

showing_cmif bool

Is the singular-value reading asked for? Only meaningful for

showing_waterfall bool

Is the 3-D reading asked for? Only meaningful where the

Source code in src/visualdynamics/gui/panes.py
def __init__(self, theme_name: str, offscreen: bool = False,
             parent: QWidget | None = None) -> None:
    super().__init__(parent)
    import pyqtgraph as pg

    self.theme_name: str = theme_name
    #: offscreen replaces the embedded 3-D data view with an
    #: off-screen plotter, exactly as ScenePane does for the scene
    self.offscreen: bool = offscreen
    #: the 3-D data surface, built on first use — None until the
    #: waterfall is first asked for
    self.waterfall_plotter: Any = None
    self._waterfall_page: QWidget | None = None
    #: None lets the data choose how it reads; a click outranks it
    self.plot_mode: str | None = None
    #: set while the pair, event or quantity box is being refilled,
    #: so restating the same list does not read as picking from it
    self._loading_pairs = False
    self._loading_events = False
    self._loading_quantities = False
    self._loading_page = False
    #: and the same for the scaling field, which is restated on
    #: every drawing of a comparison
    self._loading_scaling = False
    #: the averaging button is asked for once and stays asked for.
    #: The bar is reset before every drawing, so the button's own
    #: checked state is not somewhere the choice can be kept.
    self.averaging_wanted: bool = False
    self.shocks_wanted: bool = False
    self.filter_wanted: bool = False
    self.truncate_wanted: bool = False
    self.octave_wanted: bool = False
    #: the kurtosis reading, sticky like every other view choice
    self.kurtosis_wanted: bool = False
    #: and the scalogram, the same way
    self.wavelet_wanted: bool = False
    #: what it is computed with, kept across redraws so a tuned
    #: range survives clicking away and back
    self.wavelet_settings: dict[str, Any] | None = None
    #: where its band has been dragged to, if it has
    self.kurtosis_bounds: tuple[float, float] | None = None
    #: 'curves', 'error' or 'lines' — sticky, like the other view
    #: choices, so it survives a redraw
    self.comparison_view: str = 'curves'
    #: where the bar charts' thresholds have been dragged to
    self.error_bounds: tuple[float, float] | None = None
    self.lines_bound: float | None = None
    #: 'overlay' or 'waveform' — the transient's own
    #: readings, kept apart from the random ones above because they
    #: are different questions and a single sticky choice shared
    #: between them would carry a random answer into a transient
    self.replication_view: str = 'overlay'
    #: 'curves' or 'error' — the same for a pair of shock spectra
    self.srs_view: str = 'curves'
    #: 'overlay' or 'ratio' — how two selected densities read
    #: together; sticky like every view choice
    self.spectra_view: str = 'overlay'
    self.srs_bounds: tuple[float, float] | None = None
    #: where the replication bars' thresholds have been dragged
    self.waveform_bound: float | None = None

    self.graphics: pg.GraphicsLayoutWidget = pg.GraphicsLayoutWidget()
    # pyqtgraph's GraphicsView ignores every drag it is offered — its own
    # comment says the class "likes to consume drag events" — but the
    # widget still advertises that it takes drops. So Qt routes a drag
    # across the plot to it, gets nothing back to record as an entry, and
    # then complains on the way out: "drag leave received before drag
    # enter". Dropping files is the tree's job; the plot saying it takes
    # them was never true.
    self.graphics.setAcceptDrops(False)
    self.graphics.viewport().setAcceptDrops(False)

    self.toolbar: QToolBar = self._build_toolbar()
    # The comparison, replication and SRS readings are built
    # visible and were only ever put away by `reset_controls` —
    # harmless while the bar computed its own visibility from the
    # arguments it was handed, and wrong the moment the bar is
    # derived from what is actually on it. A control that is not a
    # live option is absent, and that has to be true from the
    # first frame, not from the first reset.
    self.show_comparison_views(False)
    self.show_spectra_views(False)
    self.show_replication_views(False)
    self.show_srs_views(False)
    # the averaging parameters sit beside the plot rather than under
    # it: they describe a span along the time axis, and reading them
    # against the shading means seeing both at once
    self.averaging_panel: AveragingPanel = AveragingPanel()
    self.averaging_panel.hide()
    self.shock_panel: ShockPanel = ShockPanel()
    self.shock_panel.hide()
    self.filter_panel: FilterPanel = FilterPanel()
    self.filter_panel.hide()
    self.truncate_panel: TruncatePanel = TruncatePanel()
    self.truncate_panel.hide()
    self.octave_panel: OctavePanel = OctavePanel()
    self.octave_panel.hide()
    self.wavelet_panel: WaveletPanel = WaveletPanel()
    self.wavelet_panel.hide()
    self.wavelet_panel.changed.connect(self._wavelet_edited)
    # the specification being written at a shape set's modal
    # coordinates, beside the plot that previews it; the toggle
    # is on the table bar, where a lone shape set's readings are
    self.author_panel: AuthorPanel = AuthorPanel()
    self.author_panel.hide()
    plot_row = QHBoxLayout()
    plot_row.setContentsMargins(0, 0, 0, 0)
    plot_row.setSpacing(0)
    # the specification sheet is the one side panel a user works
    # *in* — a grid of levels, a grid of pairs — so its width is
    # theirs to set (Brandon, 2026-09-06): a splitter between the
    # plot and the sheet, where the settings panels keep their
    # fixed width. The 3-D page joins the splitter beside the
    # graphics on first use, so the sheet sits beside either view
    self.author_split: QSplitter = QSplitter(Qt.Orientation.Horizontal)
    self.author_split.setChildrenCollapsible(False)
    self.author_split.setHandleWidth(6)
    self.author_split.addWidget(self.graphics)
    self.author_split.addWidget(self.author_panel)
    self.author_split.setStretchFactor(0, 1)
    self.author_split.setStretchFactor(1, 0)
    plot_row.addWidget(self.author_split, 1)
    plot_row.addWidget(self.averaging_panel)
    plot_row.addWidget(self.shock_panel)
    plot_row.addWidget(self.filter_panel)
    plot_row.addWidget(self.truncate_panel)
    plot_row.addWidget(self.octave_panel)
    plot_row.addWidget(self.wavelet_panel)
    self._plot_row = plot_row

    layout = QVBoxLayout(self)
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(0)
    layout.addWidget(self.toolbar)
    layout.addLayout(plot_row, 1)
Attributes
showing_wavelet property
showing_wavelet: bool

Whether the scalogram is up — asked by whoever draws.

showing_kurtosis property
showing_kurtosis: bool

Whether the bar reading is up — asked by whoever draws.

rms_wanted property
rms_wanted: bool

Whether the RMS reading of a specification is asked for — the checked state alone, so a render can decide its panes before the bar is offered.

showing_averaging property
showing_averaging: bool

Is the averaging view asked for? Only meaningful for a time history, which is why the caller checks that first.

showing_filter property
showing_filter: bool

Is the filter view asked for? Only meaningful for a time history, which is why the caller checks that first.

showing_octave property
showing_octave: bool

Is the octave-band view asked for? Only meaningful for a plain PSD or CPSD, which is why the caller checks that first.

showing_truncate property
showing_truncate: bool

Is the truncate view asked for? Only meaningful for a time history, which is why the caller checks that first.

showing_shocks property
showing_shocks: bool

Is the shock view asked for? Only meaningful for a time history, which is why the caller checks that first.

showing_cmif property
showing_cmif: bool

Is the singular-value reading asked for? Only meaningful for FRFs, which is why the caller checks that first.

showing_waterfall property
showing_waterfall: bool

Is the 3-D reading asked for? Only meaningful where the window offered it, which is why the caller checks that first.

The filter view used to outrank this and force the drawing flat — my judgement that two overlaid ribbons would occlude each other, made without drawing them. Wrong twice over (Brandon, 2026-08-25): the stage already draws paired data at one station, and a view choice that silently overrides another view choice is exactly what this interface does not do. The preview is stage geometry now, so both readings stand.

Except while the specification sheet is open: its handles live on the flat plot, so flat_only holds the drawing flat whatever the toggle says (Brandon, 2026-09-06).

Methods:
show_acts
show_acts(acts: Sequence[tuple]) -> None

The acts the selection can take, on this bar (offer_acts).

Source code in src/visualdynamics/gui/panes.py
def show_acts(self, acts: Sequence[tuple]) -> None:
    """The acts the selection can take, on this bar (`offer_acts`)."""
    offer_acts(self.toolbar, self.__dict__.setdefault('_acts', {}), acts)
    self.sync_toolbar()
offer_rms
offer_rms(offered: bool) -> None

Show the RMS toggle for a specification on its own.

Source code in src/visualdynamics/gui/panes.py
def offer_rms(self, offered: bool) -> None:
    """Show the RMS toggle for a specification on its own."""
    self.rms_action.setVisible(offered)
    if offered:
        self.toolbar.setVisible(True)
show_spectra_views
show_spectra_views(offered: bool) -> None

Offer the overlay/ratio pair, or take it away.

Source code in src/visualdynamics/gui/panes.py
def show_spectra_views(self, offered: bool) -> None:
    """Offer the overlay/ratio pair, or take it away."""
    for action in self.spectra_actions.values():
        action.setVisible(offered)
    if offered:
        self.toolbar.setVisible(True)
        self.spectra_actions[self.spectra_view].setChecked(True)
show_comparison_views
show_comparison_views(offered: bool) -> None

Offer the three readings, or none of them.

Only a specification with a measurement against it has three readings; anything else has one, and a chooser with one choice is a question with one answer.

Source code in src/visualdynamics/gui/panes.py
def show_comparison_views(self, offered: bool) -> None:
    """Offer the three readings, or none of them.

    Only a specification with a measurement against it has three
    readings; anything else has one, and a chooser with one choice
    is a question with one answer.
    """
    for action in self.comparison_actions.values():
        action.setVisible(offered)
    if offered:
        self.toolbar.setVisible(True)
        self.comparison_actions[self.comparison_view].setChecked(True)
show_scaling
show_scaling(text: str | None) -> None

Show the comparison scaling, or take it away with None.

The field is restated on every drawing, so setting the text must not read as the user typing it — hence the guard, the same one the pair and event boxes need for the same reason.

Source code in src/visualdynamics/gui/panes.py
def show_scaling(self, text: str | None) -> None:
    """Show the comparison scaling, or take it away with None.

    The field is restated on every drawing, so setting the text
    must not read as the user typing it — hence the guard, the same
    one the pair and event boxes need for the same reason.
    """
    self.scaling_action.setVisible(text is not None)
    if text is None:
        return
    self._loading_scaling = True
    try:
        self.scaling_edit.setText(text)
    finally:
        self._loading_scaling = False
    self.toolbar.setVisible(True)
show_srs_views
show_srs_views(offered: bool) -> None

Offer the two readings of a shock-spectrum comparison.

Source code in src/visualdynamics/gui/panes.py
def show_srs_views(self, offered: bool) -> None:
    """Offer the two readings of a shock-spectrum comparison."""
    for action in self.srs_actions.values():
        action.setVisible(offered)
    if offered:
        for action in self.comparison_actions.values():
            action.setVisible(False)
        self.toolbar.setVisible(True)
        self.srs_actions[self.srs_view].setChecked(True)
show_replication_views
show_replication_views(offered: bool) -> None

Offer the two readings of a replication, or neither.

Only a transient specification with a record against it has them. They hide the random three while they are up: both sets are readings of "how did this compare", and a bar offering seven of those invites reading a waveform error against an abort band that does not exist.

Source code in src/visualdynamics/gui/panes.py
def show_replication_views(self, offered: bool) -> None:
    """Offer the two readings of a replication, or neither.

    Only a transient specification with a record against it has
    them. They hide the random three while they are up: both sets
    are readings of "how did this compare", and a bar offering
    seven of those invites reading a waveform error against an
    abort band that does not exist.
    """
    for action in self.replication_actions.values():
        action.setVisible(offered)
    if offered:
        for action in self.comparison_actions.values():
            action.setVisible(False)
        self.toolbar.setVisible(True)
        self.replication_actions[self.replication_view].setChecked(True)
show_events
show_events(labels: Sequence[tuple[str, int]]) -> None

Offer these repeats, keeping the one already chosen.

Sticky by index, unlike the channel box beside it, because a repeat is its index — the third playing of the waveform is the third whatever else the record holds — where a channel is a DOF that may arrive or leave.

No repeat is marked out. Which one is the bad one depends on which reading you care about and on what the article is for, and naming one here would be putting a judgement in a list of facts — the numbers beside the plot are what to decide with.

Source code in src/visualdynamics/gui/panes.py
def show_events(self, labels: Sequence[tuple[str, int]]) -> None:
    """Offer these repeats, keeping the one already chosen.

    Sticky by index, unlike the channel box beside it, because a
    repeat *is* its index — the third playing of the waveform is
    the third whatever else the record holds — where a channel is a
    DOF that may arrive or leave.

    No repeat is marked out. Which one is the bad one depends on
    which reading you care about and on what the article is for,
    and naming one here would be putting a judgement in a list of
    facts — the numbers beside the plot are what to decide with.
    """
    wanted = list(labels)
    self.event_action.setVisible(len(wanted) > 1)
    if len(wanted) < 2:
        return
    self.toolbar.setVisible(True)
    held = self.chosen_event()
    self._loading_events = True
    try:
        self.event_box.clear()
        for index, label in enumerate(wanted):
            self.event_box.addItem(label, index)
        if held is not None and 0 <= held < len(wanted):
            self.event_box.setCurrentIndex(held)
    finally:
        self._loading_events = False
chosen_event
chosen_event() -> int | None

The repeat being looked at, or None.

Source code in src/visualdynamics/gui/panes.py
def chosen_event(self) -> int | None:
    """The repeat being looked at, or None."""
    return self.event_box.currentData()
step_event
step_event(step: int) -> None

Move the event box by step, stopping at either end.

Stopping rather than wrapping: the repeats are in time order, and running off the last one back to the first would read as having gone forwards.

Source code in src/visualdynamics/gui/panes.py
def step_event(self, step: int) -> None:
    """Move the event box by `step`, stopping at either end.

    Stopping rather than wrapping: the repeats are in time order,
    and running off the last one back to the first would read as
    having gone forwards.
    """
    count = self.event_box.count()
    if count < 2:
        return
    index = self.event_box.currentIndex() + step
    if 0 <= index < count:
        self.event_box.setCurrentIndex(index)
reset_plot_mode
reset_plot_mode() -> None

Back to letting the data choose. Called when the selection changes, so an override applies to the thing being looked at and not for ever.

Source code in src/visualdynamics/gui/panes.py
def reset_plot_mode(self) -> None:
    """Back to letting the data choose. Called when the selection
    changes, so an override applies to the thing being looked at and
    not for ever."""
    self.plot_mode = None
offer_waterfall
offer_waterfall(offered: bool) -> None

Show the 2D/3D toggle, raised by the plain-curves path itself, like show_pairs: show_controls runs before the window knows whether this drawing ends as plain curves.

Offered whenever the current reading has both forms, and withheld only where one of them does not exist — the kurtosis bars are a flat picture and a depth axis would have nothing to put along it. That is the setting half of principle 3, and the half that survives: it is the readings that stay offered whichever one is chosen (Brandon, 2026-08-27).

The scalogram has both, and opens in 3-D: it is already a function of two variables, so a surface is its natural form.

Source code in src/visualdynamics/gui/panes.py
def offer_waterfall(self, offered: bool) -> None:
    """Show the 2D/3D toggle, raised by the plain-curves path
    itself, like `show_pairs`: `show_controls` runs before the
    window knows whether this drawing ends as plain curves.

    Offered whenever the current reading has both forms, and
    withheld only where one of them does not exist — the kurtosis
    bars are a flat picture and a depth axis would have nothing to
    put along it. That is the *setting* half of principle 3, and
    the half that survives: it is the readings that stay offered
    whichever one is chosen (Brandon, 2026-08-27).

    The scalogram has both, and opens in 3-D: it is already a
    function of two variables, so a surface is its natural form.
    """
    flat = self.kurtosis_wanted or self.flat_only
    self.waterfall_action.setVisible(offered and not flat)
    if offered and not flat:
        self.toolbar.setVisible(True)
offer_frequency_axis
offer_frequency_axis(offered: bool, log: bool) -> None

Show the decades/hertz toggle for data drawn over frequency, reading log, the axis as it stands — the class's convention until the viewer chooses (core.data.frequency_axis).

Source code in src/visualdynamics/gui/panes.py
def offer_frequency_axis(self, offered: bool, log: bool) -> None:
    """Show the decades/hertz toggle for data drawn over frequency,
    reading `log`, the axis as it stands — the class's convention
    until the viewer chooses (`core.data.frequency_axis`)."""
    self.log_frequency_action.setVisible(offered)
    self.log_frequency_action.setChecked(offered and log)
    if offered:
        self.toolbar.setVisible(True)
show_quantities
show_quantities(entries: Sequence[tuple[str, Any]]) -> None

Offer a mixed object's quantity groups, keeping the choice.

entries is [(label, key)], largest group first — index 0 is the default the waterfall draws unasked. Sticky by key, not by position, like the pair box: the list is rebuilt on every drawing, and a choice that moved with the index would jump to another quantity whenever a record pick changed the counts. Hidden below two entries — one quantity is not a choice.

Source code in src/visualdynamics/gui/panes.py
def show_quantities(self, entries: Sequence[tuple[str, Any]]) -> None:
    """Offer a mixed object's quantity groups, keeping the choice.

    `entries` is [(label, key)], largest group first — index 0 is
    the default the waterfall draws unasked. Sticky by key, not by
    position, like the pair box: the list is rebuilt on every
    drawing, and a choice that moved with the index would jump to
    another quantity whenever a record pick changed the counts.
    Hidden below two entries — one quantity is not a choice.
    """
    self._loading_quantities = True
    try:
        previous = self.quantity_box.currentData()
        self.quantity_box.clear()
        for label, key in entries:
            self.quantity_box.addItem(label, key)
        if previous is not None:
            at = next((k for k in range(self.quantity_box.count())
                       if self.quantity_box.itemData(k) == previous), -1)
            if at >= 0:
                self.quantity_box.setCurrentIndex(at)
    finally:
        self._loading_quantities = False
    self.quantity_action.setVisible(len(entries) > 1)
    if len(entries) > 1:
        self.toolbar.setVisible(True)
show_pages
show_pages(page: int, pages: int) -> None

Offer the page stepper, or put it away for a single page.

page is zero-based, as the scene counts them; the box shows one-based with the total as its suffix, so it reads [<] 2 / 3 [>]. Restated on every drawing, so the guard keeps that from reading as the user turning a page.

Source code in src/visualdynamics/gui/panes.py
def show_pages(self, page: int, pages: int) -> None:
    """Offer the page stepper, or put it away for a single page.

    `page` is zero-based, as the scene counts them; the box shows
    one-based with the total as its suffix, so it reads
    `[<] 2 / 3 [>]`. Restated on every drawing, so the guard keeps
    that from reading as the user turning a page.
    """
    self._loading_page = True
    try:
        self.page_box.setMaximum(max(1, pages))
        self.page_box.setSuffix(f' / {max(1, pages)}')
        self.page_box.setValue(min(page, pages - 1) + 1)
    finally:
        self._loading_page = False
    for action in (self.page_action,):
        action.setVisible(pages > 1)
    if pages > 1:
        self.toolbar.setVisible(True)
chosen_page
chosen_page() -> int

The page on the stage, zero-based.

Source code in src/visualdynamics/gui/panes.py
def chosen_page(self) -> int:
    """The page on the stage, zero-based."""
    return self.page_box.value() - 1
chosen_quantity
chosen_quantity() -> Any

The quantity box's (dimension, hint) key, or None.

Source code in src/visualdynamics/gui/panes.py
def chosen_quantity(self) -> Any:
    """The quantity box's (dimension, hint) key, or None."""
    return self.quantity_box.currentData()
create_waterfall_plotter
create_waterfall_plotter() -> Any

Build the 3-D data surface, on first use.

The same deferral as ScenePane.create_plotter and for the same macOS reason — and safe here without a callback, because the only thing that asks for it is a toggle on this pane's own bar, which cannot be clicked before the pane is on screen. Returns the plotter, or None when it was already built.

Source code in src/visualdynamics/gui/panes.py
def create_waterfall_plotter(self) -> Any:
    """Build the 3-D data surface, on first use.

    The same deferral as `ScenePane.create_plotter` and for the
    same macOS reason — and safe here without a callback, because
    the only thing that asks for it is a toggle on this pane's own
    bar, which cannot be clicked before the pane is on screen.
    Returns the plotter, or None when it was already built.
    """
    if self.waterfall_plotter is not None:
        return None
    if self.offscreen:
        import pyvista as pv

        self.waterfall_plotter = pv.Plotter(off_screen=True)
        self._waterfall_page = QLabel('3D view disabled (offscreen mode)')
    else:
        from pyvistaqt import QtInteractor

        from ..viz import undeferred

        self.waterfall_plotter = undeferred(QtInteractor(self))
        # same refusal as the scene pane: pyvistaqt answers drops
        # with pyvista.read, which cannot read a .vdyn — the window
        # can, and gets the drop by this widget refusing it
        self.waterfall_plotter.setAcceptDrops(False)
        self.waterfall_plotter.enable_anti_aliasing('fxaa')
        self._waterfall_page = self.waterfall_plotter
    self._waterfall_page.hide()
    # beside the graphics, same stretch, so the swap keeps the size
    # — and inside the sheet's splitter, so the sheet sits beside
    # the stage exactly as it sits beside the flat plot
    self.author_split.insertWidget(1, self._waterfall_page)
    self.author_split.setStretchFactor(1, 1)
    self.author_split.setStretchFactor(2, 0)
    return self.waterfall_plotter
reserve_bottom
reserve_bottom(pixels: int) -> None

Keep the bottom pixels of the plot area empty.

The console tab floats over the views' bottom edge rather than claiming a strip, and it landed on the legend row and the bottom axis (Brandon, 2026-09-01). A layout margin is the clearance: every plot, legend and axis ends above it, and the margin survives clear(), so one call at construction holds.

Source code in src/visualdynamics/gui/panes.py
def reserve_bottom(self, pixels: int) -> None:
    """Keep the bottom `pixels` of the plot area empty.

    The console tab floats over the views' bottom edge rather than
    claiming a strip, and it landed on the legend row and the
    bottom axis (Brandon, 2026-09-01). A layout margin is the
    clearance: every plot, legend and axis ends above it, and the
    margin survives `clear()`, so one call at construction holds.
    """
    left, top, right, _ = self.graphics.ci.layout.getContentsMargins()
    self.graphics.ci.layout.setContentsMargins(left, top, right, pixels)
show_waterfall
show_waterfall(wanted: bool) -> None

Swap the plot surface: the 2-D graphics or the 3-D view.

Both stay children of the pane; only visibility moves, so the toggle costs no layout work and the hidden one keeps its state.

Source code in src/visualdynamics/gui/panes.py
def show_waterfall(self, wanted: bool) -> None:
    """Swap the plot surface: the 2-D graphics or the 3-D view.

    Both stay children of the pane; only visibility moves, so the
    toggle costs no layout work and the hidden one keeps its state.
    """
    if wanted:
        self.create_waterfall_plotter()
    if self._waterfall_page is None:
        return   # never built, and not wanted: the graphics stand
    self.graphics.setVisible(not wanted)
    self._waterfall_page.setVisible(wanted)
component
component() -> str

Which part of a complex ordinate to draw.

Source code in src/visualdynamics/gui/panes.py
def component(self) -> str:
    """Which part of a complex ordinate to draw."""
    return self.component_box.currentData()
show_pairs
show_pairs(pairs: Sequence[tuple[str, str]]) -> None

Offer these comparisons, keeping the one already chosen.

Sticky by DOF and not by position: the list is rebuilt on every drawing, and a selection that moved with the index would jump to another channel whenever one arrived or left.

This raises the bar itself, and has to. show_controls decides whether there is a bar from the controls it knows about, and it runs before this does — so a drop-down put up afterwards landed in a bar that had already been hidden. It went unnoticed because a specification used to be complex, which offered the component box, which kept the bar up for its own reasons; storing PSDs real took that away and the drop-down went with it.

A QAction's isVisible is its own flag and stays true inside a hidden toolbar, so anything checking that alone will agree the box is up while the screen shows nothing.

Source code in src/visualdynamics/gui/panes.py
def show_pairs(self, pairs: Sequence[tuple[str, str]]) -> None:
    """Offer these comparisons, keeping the one already chosen.

    Sticky by DOF and not by position: the list is rebuilt on every
    drawing, and a selection that moved with the index would jump to
    another channel whenever one arrived or left.

    This raises the bar itself, and has to. `show_controls` decides
    whether there is a bar from the controls *it* knows about, and
    it runs before this does — so a drop-down put up afterwards
    landed in a bar that had already been hidden. It went unnoticed
    because a specification used to be complex, which offered the
    component box, which kept the bar up for its own reasons;
    storing PSDs real took that away and the drop-down went with it.

    A QAction's `isVisible` is its own flag and stays true inside a
    hidden toolbar, so anything checking that alone will agree the
    box is up while the screen shows nothing.
    """
    from ..plot import pair_label

    wanted = list(pairs)
    self.pair_action.setVisible(len(wanted) > 1)
    if len(wanted) < 2:
        return
    self.toolbar.setVisible(True)
    held = self.chosen_pair()
    self._loading_pairs = True
    try:
        self.pair_box.clear()
        for pair in wanted:
            self.pair_box.addItem(pair_label(pair), pair)
        if held in wanted:
            self.pair_box.setCurrentIndex(wanted.index(held))
    finally:
        self._loading_pairs = False
chosen_pair
chosen_pair() -> tuple[str, str] | None

The comparison being looked at, or None.

Source code in src/visualdynamics/gui/panes.py
def chosen_pair(self) -> tuple[str, str] | None:
    """The comparison being looked at, or None."""
    return self.pair_box.currentData()
select_pair_label
select_pair_label(label: str) -> bool

Show the comparison this label names. True if it moved.

By label rather than by DOF pair, because the table names its rows the way the box does and matching the text is what keeps the two agreeing without either learning the other's model.

Source code in src/visualdynamics/gui/panes.py
def select_pair_label(self, label: str) -> bool:
    """Show the comparison this label names. True if it moved.

    By label rather than by DOF pair, because the table names its
    rows the way the box does and matching the text is what keeps
    the two agreeing without either learning the other's model.
    """
    from ..plot import pair_label

    for index in range(self.pair_box.count()):
        pair = self.pair_box.itemData(index)
        if pair is not None and pair_label(pair) == label:
            if index == self.pair_box.currentIndex():
                return False
            self._loading_pairs = True
            try:
                self.pair_box.setCurrentIndex(index)
            finally:
                self._loading_pairs = False
            return True
    return False
step_pair
step_pair(step: int) -> None

Move to the next comparison, or the previous one.

Stops at the ends rather than wrapping: running off the bottom of a channel list and arriving back at the top reads as nothing having happened.

Source code in src/visualdynamics/gui/panes.py
def step_pair(self, step: int) -> None:
    """Move to the next comparison, or the previous one.

    Stops at the ends rather than wrapping: running off the bottom
    of a channel list and arriving back at the top reads as nothing
    having happened.
    """
    if not self.pair_action.isVisible():
        return
    wanted = self.pair_box.currentIndex() + step
    if 0 <= wanted < self.pair_box.count():
        self.pair_box.setCurrentIndex(wanted)
sync_toolbar
sync_toolbar() -> None

The bar shows exactly when something on it does.

Derived, never decided by a caller. Five methods put controls on this bar and each knows only its own slice, so any one of them setting the bar's visibility from its own slice hides another's controls — which is exactly what happened: the fitting screen's show_pair_controls hid the whole bar whenever no shape set was co-selected, and once Residual moved onto that bar, a fit started from the FRF's own act lost it. An action's isVisible is its own property and does not follow the bar's, so the bar can be read back off its contents.

The dividers settle first and are then discounted: a separator is visible whether or not anything is around it, so a bar of nothing but dividers passes a plain any(isVisible) and stays up — which is what the first fenced group did.

Source code in src/visualdynamics/gui/panes.py
def sync_toolbar(self) -> None:
    """The bar shows exactly when something on it does.

    Derived, never decided by a caller. Five methods put controls
    on this bar and each knows only its own slice, so any one of
    them setting the bar's visibility from its own slice hides
    another's controls — which is exactly what happened: the
    fitting screen's `show_pair_controls` hid the whole bar
    whenever no shape set was co-selected, and once Residual moved
    onto that bar, a fit started from the FRF's own act lost
    it. An action's `isVisible` is its own property and does not
    follow the bar's, so the bar can be read back off its contents.

    The dividers settle first and are then discounted: a separator
    is visible whether or not anything is around it, so a bar of
    nothing but dividers passes a plain `any(isVisible)` and stays
    up — which is what the first fenced group did.
    """
    tidy(self.toolbar)
    self.toolbar.setVisible(shows_anything(self.toolbar))
show_controls
show_controls(
    *,
    map_wanted: bool | None,
    diagonal: tuple[str, str, bool] | None,
    cmif: bool,
    complex_data: bool,
    pair: bool,
    averaging: bool = False,
    shocks: bool = False,
    residual: bool = False,
    octave: bool = False,
) -> None

Show exactly the controls this data can use, and hide the bar when that is none of them.

map_wanted is True for a map, False for curves and None when the data has no such choice; diagonal is None when there is no diagonal to filter to, else (label, tooltip, already_filtered); cmif offers the singular-value reading; complex_data offers the component box; pair offers Edit Fit / Resynthesis; averaging offers the frames a time history would be cut into.

Source code in src/visualdynamics/gui/panes.py
def show_controls(self, *, map_wanted: bool | None,
                  diagonal: tuple[str, str, bool] | None, cmif: bool,
                  complex_data: bool, pair: bool,
                  averaging: bool = False, shocks: bool = False,
                  residual: bool = False,
                  octave: bool = False) -> None:
    """Show exactly the controls this data can use, and hide the bar
    when that is none of them.

    `map_wanted` is True for a map, False for curves and None when
    the data has no such choice; `diagonal` is None when there is no
    diagonal to filter to, else `(label, tooltip, already_filtered)`;
    `cmif` offers the singular-value reading; `complex_data` offers
    the component box; `pair` offers Edit Fit / Resynthesis;
    `averaging` offers the frames a time history would be cut into.
    """
    # visibility is settled by `sync_toolbar` at the end, from what
    # is actually on the bar — see the note there
    for action in (self.curves_action, self.map_action):
        action.setVisible(map_wanted is not None)
    self.drive_point_action.setVisible(diagonal is not None)
    self.cmif_action.setVisible(cmif)
    # Every reading this record can carry stays offered, whichever
    # one is currently up.
    #
    # It used to be otherwise: kurtosis replaced the trace, so the
    # marks-on-the-trace buttons were hidden while it was up as
    # having nothing to mark (2026-08-24). That reasoning was about
    # *marks* and stopped being right when the four became a group
    # of readings — the way out of a reading is to pick another
    # one, and hiding the alternatives left the group collapsed to
    # whichever button had been pressed, with no way back except
    # unpicking it first (Brandon, 2026-08-27). A fenced set of
    # five that shows one is worse than the undivided row was.
    reading = averaging or shocks
    instead = reading and (self.kurtosis_wanted or self.wavelet_wanted)
    self.averaging_action.setVisible(averaging)
    self.filter_action.setVisible(averaging)
    self.filter_action.setChecked(averaging and self.filter_wanted)
    if not averaging or instead:
        self.filter_panel.hide()
    # the cut is offered wherever the averaging is: both are
    # readings of a record's stretch of time
    self.truncate_action.setVisible(averaging)
    self.truncate_action.setChecked(averaging and self.truncate_wanted)
    if not averaging or instead:
        self.truncate_panel.hide()
    # only show_pairs puts it up, and only when there is a choice
    self.pair_action.setVisible(False)
    # and only offer_waterfall puts this one up, where plain curves
    # actually draw — registered here so no other drawing inherits it
    self.waterfall_action.setVisible(False)
    # likewise the frequency axis: offered by the series renderer
    # for data over frequency, and by nothing else
    self.log_frequency_action.setVisible(False)
    # the quantity box is the waterfall path's too, and the pager.
    # Hidden, never *reset*: the page is a view choice like every
    # other on this bar, and `show_pages` clamps the box through
    # setMaximum — so resetting here wiped the user's page a moment
    # before the drawing that was about to read it back, and the
    # arrows appeared to do nothing at all.
    self.quantity_action.setVisible(False)
    for action in (self.page_action,):
        action.setVisible(False)
    self.averaging_action.setChecked(
        averaging and not instead and self.averaging_wanted)
    if not averaging or instead:
        # the panel belongs to the button: nothing else may show it,
        # so a selection with no averaging — or one reading its
        # kurtosis instead — cannot leave one up
        self.averaging_panel.hide()
    # offered wherever the averaging is: both are readings of a
    # record, and a record is what has channels to be Gaussian
    self.kurtosis_action.setVisible(reading)
    self.kurtosis_action.setChecked(reading and self.kurtosis_wanted)
    # the scalogram is offered wherever the averaging is, and for
    # the same reason: both are readings of a record
    self.wavelet_action.setVisible(averaging)
    self.wavelet_action.setChecked(averaging and self.wavelet_wanted)
    self.shocks_action.setVisible(shocks)
    # the octave reading belongs to a plain density; offered there,
    # absent everywhere else (principle 3)
    self.octave_action.setVisible(octave)
    self.octave_action.setChecked(octave and self.octave_wanted)
    if not octave or not self.octave_wanted:
        self.octave_panel.hide()
    self.residual_action.setVisible(residual)
    self.shocks_action.setChecked(shocks and self.shocks_wanted)
    if not shocks or instead:
        self.shock_panel.hide()
    if not averaging or not self.wavelet_wanted:
        self.wavelet_panel.hide()
    # the CMIF is singular values — there is no real part to pick
    self.component_action.setVisible(
        complex_data and not (cmif and self.showing_cmif))
    self.pair_edit_action.setVisible(pair)
    self.pair_synthesis_action.setVisible(pair)
    if pair:
        self.pair_synthesis_action.setChecked(True)
    if diagonal is not None:
        # same button, same symbol, named for what the diagonal is here
        label, tooltip, filtered = diagonal
        self.drive_point_action.setText(label)
        self.drive_point_action.setToolTip(tooltip)
        # checked is derived, never stored: the button is down exactly
        # when the selection is the diagonal, so picking any other cell
        # visibly releases it
        self.drive_point_action.setChecked(filtered)
    if map_wanted is not None:
        (self.map_action if map_wanted
         else self.curves_action).setChecked(True)
    self.sync_toolbar()
reset_controls
reset_controls() -> None

Nothing applies until something says otherwise.

Called before every render, so a drawing that has no use for the bar — photos, a report — does not have to remember to put away the controls the last one left up.

Which is a promise each control has to be registered here to keep. The event box was added later and never was, so once a transient comparison raised it, it stayed up over the record on its own — where every event is on screen at once and there is nothing to choose between — and over a channel table, which has no events at all.

Source code in src/visualdynamics/gui/panes.py
def reset_controls(self) -> None:
    """Nothing applies until something says otherwise.

    Called before every render, so a drawing that has no use for the
    bar — photos, a report — does not have to remember to put away
    the controls the last one left up.

    Which is a promise each control has to be registered here to
    keep. The event box was added later and never was, so once a
    transient comparison raised it, it stayed up over the record on
    its own — where every event is on screen at once and there is
    nothing to choose between — and over a channel table, which has
    no events at all.
    """
    self.show_controls(map_wanted=None, diagonal=None, cmif=False,
                       complex_data=False, pair=False, averaging=False,
                       shocks=False)
    self.event_action.setVisible(False)
    self.scaling_action.setVisible(False)
    self.rms_action.setVisible(False)
    self.show_replication_views(False)
    self.show_srs_views(False)
    # the acts too: the render that follows puts back the ones
    # the new selection can take
    self.show_acts([])
show_pair_controls
show_pair_controls(offered: bool) -> None

The bar as the fitting screen wants it: the pair choice alone.

Source code in src/visualdynamics/gui/panes.py
def show_pair_controls(self, offered: bool) -> None:
    """The bar as the fitting screen wants it: the pair choice alone."""
    for action in (self.curves_action, self.map_action,
                   self.drive_point_action, self.cmif_action,
                   self.averaging_action, self.kurtosis_action,
                   self.shocks_action, self.filter_action,
                   self.truncate_action, self.octave_action,
                   self.scaling_action, self.waterfall_action,
                   self.log_frequency_action,
                   self.quantity_action, self.page_action):
        action.setVisible(False)
    self.pair_edit_action.setVisible(offered)
    self.pair_synthesis_action.setVisible(offered)
    if offered:
        self.pair_edit_action.setChecked(True)
    # NOT `setVisible(offered)`: this used to hide the whole bar
    # whenever no pair was offered, which was right while the
    # fitting screen's bar held nothing else — and wrong the day
    # the Residual toggle moved onto it. A fit started from the
    # FRF's own calculator has no co-selected shape set, so the
    # bar vanished and took Residual with it.
    self.sync_toolbar()

ScenePane

ScenePane(
    theme_name: str,
    offscreen: bool = False,
    parent: QWidget | None = None,
)

Bases: QWidget

The 3-D view: the scene, its camera, and how the scene is annotated.

What is in the scene stays outside — geometry, deflection and the picking that edits them all need the project, and none of them is a view's business. The pane owns the render window, the annotations drawn around whatever is in it, and the bar those annotations hang from. Anyone placing the pane can add their own actions to that bar.

Methods:

Name Description
show_acts

The acts the selection can take, on this bar (offer_acts).

offer_rigid

Show the toggle only while it applies — one whole geometry,

create_plotter

Build the embedded VTK view, once the pane is on screen.

set_bounds_visible

Toggle the labelled axes drawn around the geometry.

set_orientation_visible

Toggle the orientation triad in the corner.

apply_annotations

Re-apply annotations in place, so the camera keeps its position.

apply_background

An empty 3D view must match the theme, not VTK's white default.

Attributes:

Name Type Description
showing_rigid bool

Whether the rigid-body reading is up.

Source code in src/visualdynamics/gui/panes.py
def __init__(self, theme_name: str, offscreen: bool = False,
             parent: QWidget | None = None) -> None:
    super().__init__(parent)
    self.theme_name: str = theme_name
    #: the wish survives the selection, the data pane's rule: pick
    #: another geometry and the reading comes back up
    self.rigid_wanted: bool = False
    # the orientation triad is useful at a glance; the labelled box
    # around the geometry is clutter until asked for
    self.bounds_visible: bool = False
    self.orientation_visible: bool = True
    self.axis_unit: str = ''

    self.toolbar: QToolBar = self._build_toolbar()
    layout = QVBoxLayout(self)
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(0)
    layout.addWidget(self.toolbar)
    if offscreen:
        import pyvista as pv
        self.plotter: Any = pv.Plotter(off_screen=True)
        self._page = QLabel('3D view disabled (offscreen mode)')
    else:
        # On macOS, building the VTK widget before the window is mapped
        # leaves the whole window 0x0 and never shown; it is created in
        # create_plotter() instead, once the pane is on screen.
        self.plotter = None
        self._page = QLabel('Loading 3D view...')
    self._creating_plotter: bool = False
    # the settings panels sit beside the view rather than under
    # it, the data pane's own arrangement: the rigid-body table is
    # read against the model it is moving, and both want the height
    self.rigid_panel: RigidBodyPanel = RigidBodyPanel()
    self.rigid_panel.hide()
    row = QHBoxLayout()
    row.setContentsMargins(0, 0, 0, 0)
    row.setSpacing(0)
    row.addWidget(self._page, 1)
    row.addWidget(self.rigid_panel)
    layout.addLayout(row)
    self._layout = layout
    self._view_row = row
Attributes
showing_rigid property
showing_rigid: bool

Whether the rigid-body reading is up.

Methods:
show_acts
show_acts(acts: Sequence[tuple]) -> None

The acts the selection can take, on this bar (offer_acts).

Source code in src/visualdynamics/gui/panes.py
def show_acts(self, acts: Sequence[tuple]) -> None:
    """The acts the selection can take, on this bar (`offer_acts`)."""
    offer_acts(self.toolbar, self.__dict__.setdefault('_acts', {}), acts)
offer_rigid
offer_rigid(offered: bool) -> None

Show the toggle only while it applies — one whole geometry, nothing riding it — checked if it was wanted last time.

Source code in src/visualdynamics/gui/panes.py
def offer_rigid(self, offered: bool) -> None:
    """Show the toggle only while it applies — one whole geometry,
    nothing riding it — checked if it was wanted last time."""
    offered = bool(offered)
    self.rigid_action.setVisible(offered)
    self._rigid_fence.setVisible(offered)
    self.rigid_action.setChecked(offered and self.rigid_wanted)
    if not (offered and self.rigid_wanted):
        self.rigid_panel.hide()
create_plotter
create_plotter() -> Any

Build the embedded VTK view, once the pane is on screen.

Returns the plotter, or None if it was already built — the caller usually wants to draw into a view that has just appeared.

Source code in src/visualdynamics/gui/panes.py
def create_plotter(self) -> Any:
    """Build the embedded VTK view, once the pane is on screen.

    Returns the plotter, or None if it was already built — the caller
    usually wants to draw into a view that has just appeared.
    """
    if self.plotter is not None or self._creating_plotter:
        return None
    from pyvistaqt import QtInteractor

    from ..viz import undeferred

    placeholder = self._page
    # Re-entrant on Windows: making the VTK view's native window
    # re-shows the main window, whose showEvent lands here again
    # while the first QtInteractor is half-built — and built a
    # second, then read `renderers` off the first before it had
    # any. The first Windows launch of 0.1.0a1 died in that loop
    # (the release smoke test, 2026-09-14). One construction at a
    # time; the nested call answers None and the outer one draws.
    self._creating_plotter = True
    try:
        self.plotter = undeferred(QtInteractor(self))
    finally:
        self._creating_plotter = False
    # pyvistaqt takes drops and answers them with `pyvista.read`, so a
    # .vdyn dropped on the 3D view — aimed at the window, landing
    # here — comes back as "not able to be automatically read by
    # pyvista" and is never imported. Qt routes drags only to widgets
    # that say they take them, so refusing here sends the drop up to
    # the window, which knows what a .vdyn is. Adding a mesh to a
    # scene by dropping a file on it was never a thing this offers.
    self.plotter.setAcceptDrops(False)
    # cheap post-process pass: smooths the model's edges without the
    # supersampling alternative, which shrinks points and lines because
    # their sizes are in pixels
    self.plotter.enable_anti_aliasing('fxaa')
    self._page = self.plotter
    self.apply_background()
    self._view_row.replaceWidget(placeholder, self.plotter)
    self._view_row.setStretchFactor(self.plotter, 1)
    placeholder.deleteLater()
    return self.plotter
set_bounds_visible
set_bounds_visible(visible: bool) -> None

Toggle the labelled axes drawn around the geometry.

Source code in src/visualdynamics/gui/panes.py
def set_bounds_visible(self, visible: bool) -> None:
    """Toggle the labelled axes drawn around the geometry."""
    self.bounds_visible = bool(visible)
    self.apply_annotations()
set_orientation_visible
set_orientation_visible(visible: bool) -> None

Toggle the orientation triad in the corner.

Source code in src/visualdynamics/gui/panes.py
def set_orientation_visible(self, visible: bool) -> None:
    """Toggle the orientation triad in the corner."""
    self.orientation_visible = bool(visible)
    self.apply_annotations()
apply_annotations
apply_annotations() -> None

Re-apply annotations in place, so the camera keeps its position.

Source code in src/visualdynamics/gui/panes.py
def apply_annotations(self) -> None:
    """Re-apply annotations in place, so the camera keeps its position."""
    if self.plotter is None:
        return
    annotate_scene(self.plotter, self.axis_unit,
                   resolve_theme(self.theme_name), self.bounds_visible,
                   self.orientation_visible)
    self.plotter.render()
apply_background
apply_background() -> None

An empty 3D view must match the theme, not VTK's white default.

Source code in src/visualdynamics/gui/panes.py
def apply_background(self) -> None:
    """An empty 3D view must match the theme, not VTK's white default."""
    if self.plotter is not None:
        colors = resolve_theme(self.theme_name)
        self.plotter.set_background(colors['scene_background'],
                                    top=colors['scene_background_top'])

Functions:

offer_acts

offer_acts(
    toolbar: Any, state: dict, acts: Sequence[tuple]
) -> None

Put the acts a selection can take on a bar, in their own fenced group at its end — an icon each, like every other button on a bar, the verb and its meaning in the tooltip (Brandon, 2026-09-12: "none should [carry text] and the user should rely on tool tips to learn icons they don't recognize"; this reverses the labelled buttons of 2026-09-04). Every act on the bar, none behind a menu.

acts is [(verb, label, icon name, callback, tooltip)], in the order the bar shows them. Rebuilt on every call: a toolbar cannot reorder its actions, and the order is part of the reading — the same verb in the same place whatever else the selection allows.

Source code in src/visualdynamics/gui/panes.py
def offer_acts(toolbar: Any, state: dict, acts: Sequence[tuple]) -> None:
    """Put the acts a selection can take on a bar, in their own fenced
    group at its end — an icon each, like every other button on a
    bar, the verb and its meaning in the tooltip (Brandon, 2026-09-12:
    "none should [carry text] and the user should rely on tool tips to
    learn icons they don't recognize"; this reverses the labelled
    buttons of 2026-09-04). Every act on the bar, none behind a menu.

    `acts` is [(verb, label, icon name, callback, tooltip)], in the
    order the bar shows them. Rebuilt on every call: a toolbar cannot
    reorder its actions, and the order is part of the reading — the
    same verb in the same place whatever else the selection allows.
    """
    from PySide6.QtWidgets import QToolButton

    actions = state.setdefault('actions', {})
    for action in actions.values():
        toolbar.removeAction(action)
    if 'fence' not in state:
        state['fence'] = toolbar.addSeparator()
    state['fence'].setVisible(bool(acts))
    state['callbacks'] = {verb: callback for verb, _l, _i, callback, _t
                          in acts}
    for verb, label, icon, _callback, tooltip in acts:
        action = actions.get(verb)
        if action is None:
            action = QAction(control_icon(icon), label, toolbar)
            # through the state, so a later offer's callback is the
            # one a press runs
            action.triggered.connect(
                lambda _checked=False, v=verb: state['callbacks'][v]())
            actions[verb] = action
        action.setText(label)
        # the label leads the tooltip: the icon is what shows, and the
        # words are where a person learns what it does
        action.setToolTip(f'{label}{tooltip}' if tooltip else label)
        action.setVisible(True)
        toolbar.addAction(action)
        button = toolbar.widgetForAction(action)
        if isinstance(button, QToolButton):
            button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)