Source code for jdxi_editor.ui.widgets.slider.slider

"""
Custom Slider Widget Module

This module defines address custom slider widget (Slider) that combines address QSlider with address label and address value digital.
It offers additional functionality including:

- Customizable value digital using address format function.
- Support for vertical or horizontal orientation.
- Option to add address visual center mark for bipolar sliders.
- Customizable tick mark positions and intervals.
- Integrated signal (valueChanged) for reacting to slider value changes.

The widget is built using PySide6 and is intended for use in applications requiring address more informative slider,
such as in audio applications or other UIs where real-time feedback is important.

Usage Example:
    from your_module import Slider
    slider = Slider("Volume", 0, 100, vertical=False)
    slider.setValueDisplayFormat(lambda v: f"{v}%")
    slider.valueChanged.connect(handle_value_change)

This module requires PySide6 to be installed.
"""

from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QPainter, QPen
from PySide6.QtWidgets import (
    QHBoxLayout,
    QLabel,
    QSizePolicy,
    QSlider,
)

from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.ui.common import JDXi, QVBoxLayout, QWidget
from jdxi_editor.ui.style import JDXiUIDimensions


[docs] class Slider(QWidget): """Custom slider widget with label and value digital"""
[docs] rpn_slider_changed = Signal(int)
# Define tick positions enum to match QSlider
[docs] class TickPosition:
[docs] NoTicks = QSlider.TickPosition.NoTicks
[docs] TicksBothSides = QSlider.TickPosition.TicksBothSides
[docs] TicksAbove = QSlider.TickPosition.TicksAbove
[docs] TicksBelow = QSlider.TickPosition.TicksBelow
[docs] TicksLeft = QSlider.TickPosition.TicksLeft
[docs] TicksRight = QSlider.TickPosition.TicksRight
[docs] valueChanged = Signal(int)
[docs] value_changed = valueChanged # alias for pythonic IF
def __init__( self, label: str, min_value: int, max_value: int, midi_helper: MidiIOHelper, vertical: bool = False, show_value_label: bool = True, is_bipolar: bool = False, tooltip: str = "", draw_center_mark: bool = True, draw_tick_marks: bool = True, initial_value: int = 0, parent=None, ): super().__init__(parent)
[docs] self.label = label
[docs] self.min_value = min_value
[docs] self.max_value = max_value
[docs] self.midi_helper = midi_helper
[docs] self.value_display_format = str # Default format function
[docs] self.has_center_mark = False
[docs] self.center_value = 0
[docs] self.vertical = vertical
[docs] self.is_bipolar = is_bipolar
[docs] self.draw_center_mark = draw_center_mark
[docs] self.draw_tick_marks = draw_tick_marks
self.setToolTip(tooltip) # Main layout layout = QVBoxLayout() if vertical else QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) # Reduce margins self.setLayout(layout) # Create label self.label = QLabel(label) # Create slider
[docs] self.slider = QSlider( Qt.Orientation.Vertical if vertical else Qt.Orientation.Horizontal )
self.slider.setMinimum(min_value) self.slider.setMaximum(max_value) self.slider.valueChanged.connect(self._on_valueChanged) # Set size policy for vertical sliders if vertical: layout.addWidget(self.label) # Label is added over the slider layout.addWidget( self.slider, 1 ) # Stretch 1 so groove fills available height self.slider.setSizePolicy( QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding ) self.setFixedHeight( JDXiUIDimensions.slider_vertical.HEIGHT ) # Constant height for consistent layout layout.setAlignment(self.label, Qt.AlignmentFlag.AlignLeft) layout.setAlignment(self.slider, Qt.AlignmentFlag.AlignLeft) self.slider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.slider.setTickInterval(20) self.setMinimumWidth(JDXiUIDimensions.slider_vertical.MIN_WIDTH) self.setMaximumWidth(JDXiUIDimensions.slider_vertical.MAX_WIDTH) else: self.setMinimumHeight(JDXiUIDimensions.slider_horizontal.MIN_HEIGHT) self.setMaximumHeight(JDXiUIDimensions.slider_horizontal.MAX_HEIGHT) self.slider.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) layout.addWidget(self.label) # Label is added before the slider layout.addWidget(self.slider) # Create value digital
[docs] self.value_label = QLabel(str(min_value))
self.value_label.setMinimumWidth( JDXiUIDimensions.slider_horizontal.label.MIN_WIDTH ) if show_value_label: # Add value label if needed self.value_label.setAlignment( Qt.AlignmentFlag.AlignCenter if vertical else Qt.AlignmentFlag.AlignLeft ) layout.addWidget(self.value_label) if is_bipolar: self.value_label.setText("0") self.slider.setInvertedAppearance(False) # Apply initial value for both vertical and horizontal (was only set for horizontal) self.slider.setValue(initial_value) self._update_value_label() self.slider.valueChanged.connect(self.value_changed.emit) # self.spinbox.valueChanged.connect(self.value_changed.emit)
[docs] def setLabel(self, text: str): if hasattr(self, "label"): self.label.setText(text)
[docs] def setValueDisplayFormat(self, format_func): """Set custom format function for value digital""" self.value_display_format = format_func self._update_value_label()
[docs] def setCenterMark(self, center_value): """Set center mark for bipolar sliders""" self.has_center_mark = True self.center_value = center_value self.update()
[docs] def _on_valueChanged(self, value: int): """Handle slider value changes""" self._update_value_label() self.valueChanged.emit(value)
[docs] def _update_value_label(self): """Update the value label using current format function""" value = self.slider.value() self.value_label.setText(self.value_display_format(value))
[docs] def paintEvent(self, event): """Override paint event to draw center mark if needed""" super().paintEvent(event) painter = QPainter(self) painter.setPen(QPen(Qt.GlobalColor.darkGray, 2)) positions = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] slider_rect = self.slider.geometry() if self.has_center_mark: # Calculate center position center_pos = self.slider.style().sliderPositionFromValue( self.slider.minimum(), self.slider.maximum(), self.center_value, slider_rect.width(), ) # Draw center mark painter.drawLine( center_pos + slider_rect.x(), slider_rect.y(), center_pos + slider_rect.x(), slider_rect.y() + slider_rect.height(), ) elif self.vertical: # draw tick mark lines perpendicular to the vertical slider if self.draw_tick_marks: for position in positions: painter.drawLine( slider_rect.x(), slider_rect.y() + (position * slider_rect.height()), slider_rect.x() + slider_rect.width(), slider_rect.y() + (position * slider_rect.height()), ) else: for position in positions: painter.drawLine( slider_rect.x() + position * slider_rect.width(), slider_rect.y(), slider_rect.x() + position * slider_rect.width(), slider_rect.y() + slider_rect.height(), )
[docs] def value(self) -> int: """Get current value""" return self.slider.value()
[docs] def setValue(self, value: int): """Set current value""" self.slider.setValue(value)
[docs] def setValueSilently(self, value: int): """Set current value without emitting signals (use when updating from MIDI)""" self.slider.blockSignals(True) self.slider.setValue(value) self.slider.blockSignals(False) self._update_value_label()
[docs] def setEnabled(self, enabled: bool): """Set enabled state""" super().setEnabled(enabled) self.slider.setEnabled(enabled) self.label.setEnabled(enabled) self.value_label.setEnabled(enabled)
[docs] def setTickPosition(self, position): """Set the tick mark position on the slider""" self.slider.setTickPosition(position)
[docs] def setTickInterval(self, interval): """Set the interval between tick marks""" self.slider.setTickInterval(interval)