Skip to content

Helper Widgets

Color Button

The Color button is a button that allows users to select a color for properties in Trace. The button is shown as a blank button in its current color (may be determined by a color palette).

Clicking the button will open a color selection dialog window in the style of the user's OS. Here is where they are able to select a new color.

Right-clicking the button will reset the button's color to its "default" color, which was its initial color on creation.

ColorButton(*args, color=None, index=-1, **kwargs)

Bases: QPushButton

Custom button to allow the user to select a color. The default color is a random bright color.

Left-clicking opens a color dialog box to choose a color. Right-clicking resets the color to the default.

Parameters:

Name Type Description Default
color QColor or str

Default color for the button to use, by default None

None
index int

A value used in determining a default color, by default -1

-1
Source code in trace/widgets/color_button.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(self, *args: Any, color: QColor | str = None, index: int = -1, **kwargs) -> None:
    super().__init__(*args, **kwargs)
    if not color:
        if index >= 0:
            color = self.index_color(index)
        else:
            color = self.random_color()
    elif not isinstance(color, QColor):
        color = QColor(color)

    self._color = None
    self._default = color
    self.dialog_box = QColorDialog(self)
    self.dialog_box.setCurrentColor(color)

    self.pressed.connect(self.dialog_box.show)
    self.dialog_box.colorSelected.connect(lambda c: setattr(self, "color", c))

    self.color = self._default

color property writable

The current color as a QColor.

mousePressEvent(e)

Set the color to the default on right-click.

Source code in trace/widgets/color_button.py
71
72
73
74
75
76
77
def mousePressEvent(self, e: QMouseEvent) -> None:
    """Set the color to the default on right-click."""
    if e.button() == Qt.RightButton:
        self.color = self._default
        return

    return super().mousePressEvent(e)

random_color() staticmethod

Pick a random color for the default color of each PV. This function ensures that the color is bright.

Returns:

Type Description
QColor

A random color that is guaranteed to be "bright"

Source code in trace/widgets/color_button.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@staticmethod
def random_color() -> QColor:
    """Pick a random color for the default color of each PV. This
    function ensures that the color is bright.

    Returns
    -------
    QColor
        A random color that is guaranteed to be "bright"
    """
    hue = int(360 * random())
    saturation = int(256 * (0.5 + random() / 2.0))
    lightness = int(256 * (0.4 + random() / 5.0))
    color = QColor()
    color.setHsl(hue, saturation, lightness)
    return color

index_color(index, palette='default') staticmethod

Returns the color in the color palette at index. If the requested index is larger than the size of the color palette, then the palette is cycled through again, but darker by a factor of 35%. If palette str is not a key in color_palette dict from trace/config, it will be replaced with 'default'

Parameters:

Name Type Description Default
index int

Requested index of color palette

required
palette str

Name of selected palette

'default'
Source code in trace/widgets/color_button.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@staticmethod
def index_color(index: int, palette: str = "default") -> QColor:
    """Returns the color in the color palette at index. If the
    requested index is larger than the size of the color palette, then
    the palette is cycled through again, but darker by a factor of 35%.
    If palette str is not a key in color_palette dict from trace/config,
    it will be replaced with 'default'

    Parameters
    ----------
    index : int
        Requested index of color palette
    palette : str
        Name of selected palette
    """
    if palette not in color_palette:
        palette = "default"
    modded_index = index % len(color_palette[palette])
    color = color_palette[palette][modded_index]

    dark_factor = (index // len(color_palette[palette])) * 35
    return color.darker(100 + dark_factor)

Toggle Switch

The Toggle is a widget that works the same as a checkbox, but is represented as a switch. This looks a bit nicer for the UI.

ToggleSwitch(text='', parent=None, color=None)

Bases: QCheckBox

A custom toggle switch widget that looks like a modern mobile switch. This widget extends QCheckBox to create a toggle switch with animated transition between on and off states. The switch consists of a rounded rectangle track and a circular knob that moves horizontally.

Attributes:

Name Type Description
TRACK_OFF QColor

Color of the track when the switch is off.

TRACK_ON QColor

Color of the track when the switch is on.

DIAMETER int

Diameter of the circular knob in pixels.

MARGIN int

Margin between the knob and the track edge in pixels.

Parameters:

Name Type Description Default
text str

The text label (for compatibility with QCheckBox, but not displayed)

''
parent QWidget

The parent widget.

None
color QColor

Custom color for the "on" state. If None, uses default blue.

None
Source code in trace/widgets/toggle.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def __init__(self, text: str = "", parent: Optional[Any] = None, color: Optional[QColor] = None) -> None:
    """Initialize the toggle switch widget.

    Parameters
    ----------
    text : str, optional
        The text label (for compatibility with QCheckBox, but not displayed)
    parent : QWidget, optional
        The parent widget.
    color : QColor, optional
        Custom color for the "on" state. If None, uses default blue.
    """
    if isinstance(text, (type(None), object)) and not isinstance(text, str):
        parent = text
        text = ""
    super().__init__(parent)
    self.setFixedSize(46, 26)
    self.setCursor(Qt.PointingHandCursor)
    self._x = self.MARGIN
    self._anim = QPropertyAnimation(self, b"offset", self)
    self._anim.setDuration(120)

    self._track_on_color = color if color is not None else self.TRACK_ON

getOffset()

Get the current horizontal offset of the knob.

Returns:

Type Description
int

The current x-coordinate of the knob.

Source code in trace/widgets/toggle.py
55
56
57
58
59
60
61
62
63
def getOffset(self) -> int:
    """Get the current horizontal offset of the knob.

    Returns
    -------
    int
        The current x-coordinate of the knob.
    """
    return self._x

setOffset(x)

Set the horizontal offset of the knob and update the widget.

Parameters:

Name Type Description Default
x int

The new x-coordinate for the knob.

required
Source code in trace/widgets/toggle.py
65
66
67
68
69
70
71
72
73
74
def setOffset(self, x: int) -> None:
    """Set the horizontal offset of the knob and update the widget.

    Parameters
    ----------
    x : int
        The new x-coordinate for the knob.
    """
    self._x = x
    self.update()

nextCheckState()

Handle the toggle state change and animate the knob movement. This method is called when the checkbox state changes and manages the animation of the knob from one position to another.

Source code in trace/widgets/toggle.py
78
79
80
81
82
83
84
85
86
87
88
89
90
def nextCheckState(self) -> None:
    """Handle the toggle state change and animate the knob movement.
    This method is called when the checkbox state changes and
    manages the animation of the knob from one position to another.
    """
    super().nextCheckState()
    start = self._x
    end = self.width() - self.DIAMETER - self.MARGIN if self.isChecked() else self.MARGIN

    self._anim.stop()
    self._anim.setStartValue(start)
    self._anim.setEndValue(end)
    self._anim.start()

setChecked(checked)

Override setChecked to handle programmatic state changes with animation.

Source code in trace/widgets/toggle.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def setChecked(self, checked: bool) -> None:
    """Override setChecked to handle programmatic state changes with animation."""
    if self.isChecked() != checked:
        super().setChecked(checked)
        start = self._x
        end = self.width() - self.DIAMETER - self.MARGIN if checked else self.MARGIN
        self._anim.stop()
        self._anim.setStartValue(start)
        self._anim.setEndValue(end)
        self._anim.start()

setCheckState(state)

Override setCheckState to handle Qt.CheckState enums properly in PySide6.

Source code in trace/widgets/toggle.py
103
104
105
106
107
108
109
110
def setCheckState(self, state) -> None:
    """Override setCheckState to handle Qt.CheckState enums properly in PySide6."""
    if isinstance(state, int):
        checked = state != 0
    else:
        checked = state != Qt.Unchecked

    self.setChecked(checked)

setColor(color)

Set the color for the "on" state of the toggle switch.

Parameters:

Name Type Description Default
color QColor

The color to use when the toggle is in the "on" state

required
Source code in trace/widgets/toggle.py
112
113
114
115
116
117
118
119
120
121
def setColor(self, color: QColor) -> None:
    """Set the color for the "on" state of the toggle switch.

    Parameters
    ----------
    color : QColor
        The color to use when the toggle is in the "on" state
    """
    self._track_on_color = color
    self.update()

getColor()

Get the current "on" state color.

Returns:

Type Description
QColor

The current color used for the "on" state

Source code in trace/widgets/toggle.py
123
124
125
126
127
128
129
130
131
def getColor(self) -> QColor:
    """Get the current "on" state color.

    Returns
    -------
    QColor
        The current color used for the "on" state
    """
    return self._track_on_color

paintEvent(_)

Paint the toggle switch with the appropriate colors and position.

Parameters:

Name Type Description Default
_ QPaintEvent

The paint event (unused).

required
Source code in trace/widgets/toggle.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def paintEvent(self, _: Any) -> None:
    """Paint the toggle switch with the appropriate colors and position.

    Parameters
    ----------
    _ : QPaintEvent
        The paint event (unused).
    """
    p = QPainter(self)
    p.setRenderHint(QPainter.Antialiasing)

    track_col = self._track_on_color if self.isChecked() else self.TRACK_OFF
    p.setPen(Qt.NoPen)
    p.setBrush(QColor(track_col))
    p.drawRoundedRect(self.rect(), self.height() / 2, self.height() / 2)

    # Draw the knob
    knob_rect = QRect(self._x, self.MARGIN, self.DIAMETER, self.DIAMETER)
    p.setBrush(Qt.white)
    p.drawEllipse(knob_rect)

hitButton(pos)

Determine if the given position is on the button. This is overridden to make the entire widget clickable, not just the standard checkbox indicator area.

Parameters:

Name Type Description Default
pos QPoint

The position to test.

required

Returns:

Type Description
bool

True if the position is within the widget's area, False otherwise.

Source code in trace/widgets/toggle.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def hitButton(self, pos: Any) -> bool:
    """Determine if the given position is on the button. This is
    overridden to make the entire widget clickable, not just the
    standard checkbox indicator area.

    Parameters
    ----------
    pos : QPoint
        The position to test.

    Returns
    -------
    bool
        True if the position is within the widget's area, False otherwise.
    """
    return self.contentsRect().contains(pos)

Frozen Table View

The Frozen Table View is an object that will display tabular data while keeping the leftmost column visible on the table at all times. This prevents users from scrolling left-right and hiding the leftmost column.

FrozenTableView(model)

Bases: QTableView

QTableView with the leftmost column frozen so it always shows while the rest of the table is horizontally scrollable.

Python version of Qt FreezeTableWidget example: https://doc.qt.io/qt-6/qtwidgets-itemviews-frozencolumn-example.html

Parameters:

Name Type Description Default
model QAbstractTableModel

The data model for the table

required
Source code in trace/widgets/frozen_table_view.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def __init__(self, model):
    """Initialize the frozen table view with the given model.

    Parameters
    ----------
    model : QAbstractTableModel
        The data model for the table
    """
    super(FrozenTableView, self).__init__()
    self.setModel(model)
    self.frozenTableView = QTableView(self)
    self.init()
    self.horizontalHeader().sectionResized.connect(self.updateSectionWidth)
    self.verticalHeader().hide()
    self.frozenTableView.verticalScrollBar().valueChanged.connect(self.verticalScrollBar().setValue)
    self.verticalScrollBar().valueChanged.connect(self.frozenTableView.verticalScrollBar().setValue)

init()

Initialize the frozen table view layout and properties.

Source code in trace/widgets/frozen_table_view.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def init(self) -> None:
    """Initialize the frozen table view layout and properties."""
    self.frozenTableView.setModel(self.model())
    self.frozenTableView.setFocusPolicy(Qt.NoFocus)
    self.frozenTableView.verticalHeader().hide()
    self.frozenTableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
    self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
    self.viewport().stackUnder(self.frozenTableView)

    self.setAlternatingRowColors(True)
    self.frozenTableView.setAlternatingRowColors(True)
    self.frozenTableView.setStyleSheet("QTableView {border: none; border-right: 1px solid lightGray}")

    self.setSelectionBehavior(QTableView.SelectRows)
    self.frozenTableView.setSelectionBehavior(QTableView.SelectRows)
    self.frozenTableView.setSelectionModel(self.selectionModel())
    for col in range(1, self.model().columnCount()):
        self.frozenTableView.setColumnHidden(col, True)
    self.frozenTableView.setColumnWidth(0, self.columnWidth(0))
    self.frozenTableView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    self.frozenTableView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    self.frozenTableView.show()
    self.updateFrozenTableGeometry()
    self.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
    self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
    self.frozenTableView.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)

updateSectionWidth(logicalIndex, oldSize, newSize)

Update the width of the frozen column when the main table column is resized.

Parameters:

Name Type Description Default
logicalIndex int

The logical index of the column being resized

required
oldSize int

The previous width of the column, unused

required
newSize int

The new width of the column

required
Source code in trace/widgets/frozen_table_view.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def updateSectionWidth(self, logicalIndex, oldSize, newSize) -> None:
    """Update the width of the frozen column when the main table column is resized.

    Parameters
    ----------
    logicalIndex : int
        The logical index of the column being resized
    oldSize : int
        The previous width of the column, unused
    newSize : int
        The new width of the column
    """
    if logicalIndex == 0:
        self.frozenTableView.setColumnWidth(0, newSize)
        self.updateFrozenTableGeometry()

updateSectionHeight(logicalIndex, oldSize, newSize)

Update the height of a row in the frozen table.

Parameters:

Name Type Description Default
logicalIndex int

The logical index of the row being resized

required
oldSize int

The previous height of the row, unused

required
newSize int

The new height of the row

required
Source code in trace/widgets/frozen_table_view.py
73
74
75
76
77
78
79
80
81
82
83
84
85
def updateSectionHeight(self, logicalIndex, oldSize, newSize) -> None:
    """Update the height of a row in the frozen table.

    Parameters
    ----------
    logicalIndex : int
        The logical index of the row being resized
    oldSize : int
        The previous height of the row, unused
    newSize : int
        The new height of the row
    """
    self.frozenTableView.setRowHeight(logicalIndex, newSize)

resizeEvent(event)

Handle resize events by updating the frozen table geometry.

Source code in trace/widgets/frozen_table_view.py
87
88
89
90
def resizeEvent(self, event) -> None:
    """Handle resize events by updating the frozen table geometry."""
    super(FrozenTableView, self).resizeEvent(event)
    self.updateFrozenTableGeometry()

moveCursor(cursorAction, modifiers)

Handle cursor movement with special logic for the frozen column.

Parameters:

Name Type Description Default
cursorAction CursorAction

The cursor action being performed

required
modifiers KeyboardModifiers

Keyboard modifiers

required

Returns:

Type Description
QModelIndex

The new cursor position

Source code in trace/widgets/frozen_table_view.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def moveCursor(self, cursorAction, modifiers) -> QModelIndex:
    """Handle cursor movement with special logic for the frozen column.

    Parameters
    ----------
    cursorAction : QAbstractItemView.CursorAction
        The cursor action being performed
    modifiers : Qt.KeyboardModifiers
        Keyboard modifiers

    Returns
    -------
    QModelIndex
        The new cursor position
    """
    current = super(FrozenTableView, self).moveCursor(cursorAction, modifiers)
    if (
        cursorAction == self.MoveLeft
        and current.column() > 0
        and self.visualRect(current).topLeft().x() < self.frozenTableView.columnWidth(0)
    ):
        newValue = (
            self.horizontalScrollBar().value()
            + self.visualRect(current).topLeft().x()
            - self.frozenTableView.columnWidth(0)
        )
        self.horizontalScrollBar().setValue(newValue)
    return current

scrollTo(index, hint)

Scroll to the given index, but only if it's not in the frozen column.

Parameters:

Name Type Description Default
index QModelIndex

The index to scroll to

required
hint ScrollHint

The scroll hint

required
Source code in trace/widgets/frozen_table_view.py
121
122
123
124
125
126
127
128
129
130
131
132
def scrollTo(self, index, hint):
    """Scroll to the given index, but only if it's not in the frozen column.

    Parameters
    ----------
    index : QModelIndex
        The index to scroll to
    hint : QAbstractItemView.ScrollHint
        The scroll hint
    """
    if index.column() > 0:
        super(FrozenTableView, self).scrollTo(index, hint)

updateFrozenTableGeometry()

Update the geometry of the frozen table to match the main table.

Source code in trace/widgets/frozen_table_view.py
134
135
136
137
138
139
140
141
def updateFrozenTableGeometry(self) -> None:
    """Update the geometry of the frozen table to match the main table."""
    self.frozenTableView.setGeometry(
        self.verticalHeader().width() + self.frameWidth(),
        self.frameWidth(),
        self.columnWidth(0),
        self.viewport().height() + self.horizontalHeader().height(),
    )