from LUIObject import LUIObject from LUISprite import LUISprite from LUIInitialState import LUIInitialState from LUILayouts import LUIHorizontalStretchedLayout class LUISlider(LUIObject): """ Slider which can be used to control values """ KNOB_Y_OFFSET = -5.0 def __init__(self, parent=None, filled=True, min_value=0.0, max_value=1.0, width=100.0, value=None, **kwargs): """ Constructs a new slider. If filled is True, the part behind the knob will be solid """ LUIObject.__init__(self, x=0, y=0, solid=True) self.set_width(width) self._knob = LUISprite(self, "SliderKnob", "skin") self._knob.z_offset = 2 self._knob.solid = True # Construct the background self._slider_bg = LUIHorizontalStretchedLayout(parent=self, prefix="SliderBg", center_vertical=True, width="100%", margin=(-1, 0, 0, 0)) self._filled = filled self._min_value = min_value self._max_value = max_value self._side_margin = self._knob.width / 4 self._effective_width = self.width - 2 * self._side_margin if self._filled: self._slider_fill = LUIObject(self) self._fill_left = LUISprite(self._slider_fill, "SliderBgFill_Left", "skin") self._fill_mid = LUISprite(self._slider_fill, "SliderBgFill", "skin") self._fill_mid.left = self._fill_left.width self._slider_fill.z_offset = 1 self._slider_fill.center_vertical = True self._slider_fill.width = "100%" self._slider_fill.height = "100%" if parent is not None: self.parent = parent # Handle various events self._knob.bind("mousedown", self._start_drag) self._knob.bind("mousemove", self._update_drag) self._knob.bind("mouseup", self._stop_drag) self._knob.bind("keydown", self._on_keydown) self._knob.bind("blur", self._stop_drag) self._knob.bind("keyrepeat", self._on_keydown) self._drag_start_pos = None self._dragging = False self._drag_start_val = 0 self.current_val = 10 # Set initial value if value is None: self.set_value( (self._min_value + self._max_value) / 2.0 ) else: self.set_value(value) self._update_knob() LUIInitialState.init(self, kwargs) self._refresh_layout() def on_click(self, event): """ Internal on click handler """ # I don't like this behaviour # relative_pos = self.get_relative_pos(event.coordinates) # if not self._dragging: # self._set_current_val(relative_pos.x) def _update_knob(self): """ Internal method to update the slider knob """ self._knob.left = self.current_val - (self._knob.width / 2) + self._side_margin if self._filled: knob_center = self._knob.left + (self._knob.width / 2) fill_len = max(0, knob_center - self._fill_left.width) self._fill_mid.width = fill_len if hasattr(self._slider_fill, 'left'): self._slider_fill.left = 0 def _refresh_layout(self): """Recalculate slider layout after size changes.""" # Guard for early init before internal parts are created if not hasattr(self, "_knob"): return try: # Use half knob width so knob center aligns with bar ends self._side_margin = self._knob.width / 2 except Exception: self._side_margin = 0 try: self._effective_width = max(1, self.width - (self._knob.width if hasattr(self, "_knob") else 0)) except Exception: self._effective_width = 1 if hasattr(self, "_slider_bg"): if hasattr(self._slider_bg, 'width'): self._slider_bg.width = "100%" if hasattr(self._slider_bg, 'height'): self._slider_bg.height = "100%" if hasattr(self._slider_bg, 'center_vertical'): self._slider_bg.center_vertical = True if hasattr(self._knob, 'center_vertical'): self._knob.center_vertical = True # Explicit vertical centering for knob to avoid mismatch after resize try: base_h = self.height base_top = 0 if hasattr(self, "_slider_bg") and self._slider_bg is not None: bg_h = getattr(self._slider_bg, "height", None) if bg_h is not None and bg_h != "100%": base_h = bg_h base_top = getattr(self._slider_bg, "top", 0) or 0 if hasattr(self._knob, "height"): # Visual offset compensates for the skin's knob pivot self._knob.top = (base_h - self._knob.height) / 2.0 + self.KNOB_Y_OFFSET except Exception: pass if self._filled and hasattr(self, '_slider_fill'): self._slider_fill.center_vertical = True try: base_h = self.height base_top = 0 if hasattr(self, "_slider_bg") and self._slider_bg is not None: bg_h = getattr(self._slider_bg, "height", None) if bg_h is not None and bg_h != "100%": base_h = bg_h base_top = getattr(self._slider_bg, "top", 0) or 0 if hasattr(self._slider_fill, 'height'): self._slider_fill.top = base_top + (base_h - self._slider_fill.height) / 2.0 + self.KNOB_Y_OFFSET except Exception: pass if hasattr(self._slider_fill, 'width'): self._slider_fill.width = "100%" if hasattr(self._slider_fill, 'height'): self._slider_fill.height = "100%" try: self.current_val = max(0, min(self.current_val, self._effective_width)) except Exception: pass self._update_knob() def set_width(self, width): """Set slider width and refresh layout.""" self.width = width if hasattr(self, "_slider_bg"): self._refresh_layout() def set_height(self, height): """Set slider height and refresh layout.""" self.height = height if hasattr(self, "_slider_bg"): self._refresh_layout() def _set_current_val(self, pixels): """ Internal method to set the current value in pixels """ pixels = max(0, min(self._effective_width, pixels)) self.current_val = pixels self.trigger_event("changed") self._update_knob() def _start_drag(self, event): """ Internal drag start handler """ self._knob.request_focus() if not self._dragging: self._drag_start_pos = event.coordinates self._dragging = True self._drag_start_val = self.current_val self._knob.color = (0.8,0.8,0.8,1.0) def set_value(self, value): """ Sets the value of the slider, should be between minimum and maximum. """ scaled = (float(value) - float(self._min_value)) \ / (float(self._max_value) - float(self._min_value)) \ * self._effective_width self._set_current_val(scaled) def get_value(self): """ Returns the current value of the slider """ return (self.current_val / float(self._effective_width)) \ * (float(self._max_value) - float(self._min_value)) \ + self._min_value value = property(get_value, set_value) def _on_keydown(self, event): """ Internal keydown handler """ if event.message == "arrow_right": self._set_current_val(self.current_val + 2) elif event.message == "arrow_left": self._set_current_val(self.current_val - 2) elif event.message == "escape": self.current_val = self._drag_start_val self._stop_drag(event) self._update_knob() def _update_drag(self, event): """ Internal drag handler """ if self._dragging: dragOffset = event.coordinates.x - self._drag_start_pos.x finalValue = self._drag_start_val + dragOffset self._set_current_val(finalValue) def _stop_drag(self, event): """ Internal drag stop handelr """ self._drag_start_pos = None self._dragging = False self._drag_start_val = self.current_val self._knob.color = (1,1,1,1)