Skip to content

GUI

ControlSpec

Source code in mmm_utils/GUI.py
class ControlSpec:
    def __init__(self, min: float, max: float, exp: float = 1.0):
        if min >= max:
            raise ValueError("ControlSpec min must be less than max")
        if exp <= 0:
            raise ValueError("ControlSpec exp must be positive")
        self.min = min
        self.max = max
        self.exp = exp

    def normalize(self, val: float) -> float:
        """Normalize a value to the range [0.0, 1.0] based on the control spec."""
        norm_val = scale(val, self.min, self.max, 0.0, 1.0)
        return clip(norm_val ** self.exp, 0.0, 1.0)

    def unnormalize(self, norm_val: float) -> float:
        """Convert a normalized value [0.0, 1.0] back to the control spec range."""
        norm_val = clip(norm_val, 0.0, 1.0) ** (1.0 / self.exp)
        return scale(norm_val, 0.0, 1.0, self.min, self.max)

normalize(val)

Normalize a value to the range [0.0, 1.0] based on the control spec.

Source code in mmm_utils/GUI.py
def normalize(self, val: float) -> float:
    """Normalize a value to the range [0.0, 1.0] based on the control spec."""
    norm_val = scale(val, self.min, self.max, 0.0, 1.0)
    return clip(norm_val ** self.exp, 0.0, 1.0)

unnormalize(norm_val)

Convert a normalized value [0.0, 1.0] back to the control spec range.

Source code in mmm_utils/GUI.py
def unnormalize(self, norm_val: float) -> float:
    """Convert a normalized value [0.0, 1.0] back to the control spec range."""
    norm_val = clip(norm_val, 0.0, 1.0) ** (1.0 / self.exp)
    return scale(norm_val, 0.0, 1.0, self.min, self.max)

Slider2D

Bases: QWidget


              flowchart TD
              mmm_utils.GUI.Slider2D[Slider2D]

              

              click mmm_utils.GUI.Slider2D href "" "mmm_utils.GUI.Slider2D"
            

A custom 2D slider widget

Source code in mmm_utils/GUI.py
class Slider2D(QWidget):
    """A custom 2D slider widget"""

    # Signal emitted when the slider value changes
    value_changed = Signal(float, float)
    mouse_updown = Signal(bool)

    def __init__(self, width=300, height=300, parent=None):
        super().__init__(parent)
        self.setMinimumSize(width, height)
        self.setMaximumSize(width, height)

        # Slider position (0.0 to 1.0 for both x and y)
        self._x = 0.5
        self._y = 0.5

        self._handle_radius = 10

    def get_values(self):
        """Get current X and Y values (0.0 to 1.0)"""
        return self._x, self._y

    def set_values(self, x, y):
        """Set X and Y values (0.0 to 1.0)"""
        self._x = max(0.0, min(1.0, x))
        self._y = max(0.0, min(1.0, y))
        self.update()
        self.value_changed.emit(self._x, self._y)

    def _pos_to_values(self, pos):
        """Convert widget position to slider values"""
        margin = self._handle_radius
        width = self.width() - 2 * margin
        height = self.height() - 2 * margin

        x = (pos.x() - margin) / width
        y = 1.0 - (pos.y() - margin) / height  # Invert Y so bottom is 0

        return max(0.0, min(1.0, x)), max(0.0, min(1.0, y))

    def _values_to_pos(self):
        """Convert slider values to widget position"""
        margin = self._handle_radius
        width = self.width() - 2 * margin
        height = self.height() - 2 * margin

        x = margin + self._x * width
        y = margin + (1.0 - self._y) * height  # Invert Y so bottom is 0

        return QPointF(x, y)

    def paintEvent(self, event):
        """Paint the slider"""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        # Draw background
        painter.fillRect(self.rect(), Qt.lightGray)

        # Draw border
        pen = QPen(Qt.black, 2)
        painter.setPen(pen)
        painter.drawRect(self.rect())

        # Draw grid lines
        pen = QPen(Qt.gray, 1)
        painter.setPen(pen)

        # Vertical lines
        for i in range(1, 4):
            x = self.width() * i / 4
            painter.drawLine(x, 0, x, self.height())

        # Horizontal lines
        for i in range(1, 4):
            y = self.height() * i / 4
            painter.drawLine(0, y, self.width(), y)

        # Draw center lines
        pen = QPen(Qt.darkGray, 2)
        painter.setPen(pen)
        center_x = self.width() / 2
        center_y = self.height() / 2
        painter.drawLine(center_x, 0, center_x, self.height())
        painter.drawLine(0, center_y, self.width(), center_y)

        # Draw handle
        handle_pos = self._values_to_pos()
        brush = QBrush(Qt.blue)
        painter.setBrush(brush)
        pen = QPen(Qt.darkBlue, 2)
        painter.setPen(pen)
        painter.drawEllipse(handle_pos, self._handle_radius, self._handle_radius)

    def mousePressEvent(self, event):
        """Handle mouse press"""
        if event.button() == Qt.LeftButton:
            self.mouse_updown.emit(True)
            x, y = self._pos_to_values(event.position())
            self.set_values(x, y)

    def mouseMoveEvent(self, event):
        """Handle mouse move"""
        if self.mouse_updown:
            x, y = self._pos_to_values(event.position())
            self.set_values(x, y)

    def mouseReleaseEvent(self, event):
        """Handle mouse release"""
        if event.button() == Qt.LeftButton:
            self.mouse_updown.emit(False)

get_values()

Get current X and Y values (0.0 to 1.0)

Source code in mmm_utils/GUI.py
def get_values(self):
    """Get current X and Y values (0.0 to 1.0)"""
    return self._x, self._y

mouseMoveEvent(event)

Handle mouse move

Source code in mmm_utils/GUI.py
def mouseMoveEvent(self, event):
    """Handle mouse move"""
    if self.mouse_updown:
        x, y = self._pos_to_values(event.position())
        self.set_values(x, y)

mousePressEvent(event)

Handle mouse press

Source code in mmm_utils/GUI.py
def mousePressEvent(self, event):
    """Handle mouse press"""
    if event.button() == Qt.LeftButton:
        self.mouse_updown.emit(True)
        x, y = self._pos_to_values(event.position())
        self.set_values(x, y)

mouseReleaseEvent(event)

Handle mouse release

Source code in mmm_utils/GUI.py
def mouseReleaseEvent(self, event):
    """Handle mouse release"""
    if event.button() == Qt.LeftButton:
        self.mouse_updown.emit(False)

paintEvent(event)

Paint the slider

Source code in mmm_utils/GUI.py
def paintEvent(self, event):
    """Paint the slider"""
    painter = QPainter(self)
    painter.setRenderHint(QPainter.Antialiasing)

    # Draw background
    painter.fillRect(self.rect(), Qt.lightGray)

    # Draw border
    pen = QPen(Qt.black, 2)
    painter.setPen(pen)
    painter.drawRect(self.rect())

    # Draw grid lines
    pen = QPen(Qt.gray, 1)
    painter.setPen(pen)

    # Vertical lines
    for i in range(1, 4):
        x = self.width() * i / 4
        painter.drawLine(x, 0, x, self.height())

    # Horizontal lines
    for i in range(1, 4):
        y = self.height() * i / 4
        painter.drawLine(0, y, self.width(), y)

    # Draw center lines
    pen = QPen(Qt.darkGray, 2)
    painter.setPen(pen)
    center_x = self.width() / 2
    center_y = self.height() / 2
    painter.drawLine(center_x, 0, center_x, self.height())
    painter.drawLine(0, center_y, self.width(), center_y)

    # Draw handle
    handle_pos = self._values_to_pos()
    brush = QBrush(Qt.blue)
    painter.setBrush(brush)
    pen = QPen(Qt.darkBlue, 2)
    painter.setPen(pen)
    painter.drawEllipse(handle_pos, self._handle_radius, self._handle_radius)

set_values(x, y)

Set X and Y values (0.0 to 1.0)

Source code in mmm_utils/GUI.py
def set_values(self, x, y):
    """Set X and Y values (0.0 to 1.0)"""
    self._x = max(0.0, min(1.0, x))
    self._y = max(0.0, min(1.0, y))
    self.update()
    self.value_changed.emit(self._x, self._y)