Files
krowlog/src/ui/rangeslider.py
Andreas Huber 8ce0c1bf9e switch direction of the range start/end icons
With the old direction they were overlapping each other,
which made it impossible to move the end slider.
2025-03-24 20:07:35 +01:00

192 lines
7.6 KiB
Python

import math
from enum import Enum
import PySide6
from PySide6 import QtGui
from PySide6.QtCore import QRect, QPoint, Signal
from PySide6.QtGui import QPainter, Qt, QColor, QPen
from PySide6.QtWidgets import QWidget
from src.pluginregistry import PluginRegistry
from src.util import conversion
from src.util.color import to_qcolor
from src.i18n import _
class RangeSliderHandle():
def __init__(self, value: int):
self.value = value
class UpdateStyle(Enum):
KEEP_ABSOLUTE = 2
"""
The absolute values for lower/upper are not changed unless the values would no longer be in range
"""
KEEP_ABSOLUTE_MIN_MAX = 4
"""
Like UpdateStyle.KEEP_ABSOLUTE but if lower was at min_value (or upper was at max_value), then it will stay there.
"""
class RangeSlider(QWidget):
value_changed = Signal(str, str)
"""Signal emitted when the range slider value changes.
**Note**: The value is a string and must be parsed into an int.
QT's signal api only supports 32bit integers. Ints larger
than 2**32-1 will overflow. Probably because there is some C/C++
code involved. We work around this by converting the python int
into a string."""
_width = 20
_handle_width = 12
def __init__(self):
super(RangeSlider, self).__init__()
self.setFixedWidth(self._width)
self.min_value = 0
self.max_value = 100
self.lower_value = RangeSliderHandle(0)
self.upper_value = RangeSliderHandle(self.max_value)
self.selected_handle = None
self.selection_drag_range = (self.min_value, self.max_value)
self.drag_y_offset_in_handle = 0
self._hit_positions: set[float] = set(())
def set_maximum(self, max: int):
if self.max_value == max:
return
was_at_max = self.upper_value.value == self.max_value
self.max_value = max
if was_at_max:
self.upper_value.value = max
self._emit_value_changed()
def update_hit_positions(self, hit_positions: set[float]):
# print(f"updated hit positions in range slider:{len(hit_positions)} -> {hit_positions}")
self._hit_positions = hit_positions
def paintEvent(self, event: PySide6.QtGui.QPaintEvent) -> None:
painter = QPainter(self)
self._draw_background(painter)
self._draw_hits(painter)
self._draw_handle(painter, self.lower_value, direction=-1)
self._draw_handle(painter, self.upper_value)
painter.end()
def _draw_background(self, painter: QPainter) -> None:
painter.setBrush(to_qcolor("50ade8"))
painter.setPen(to_qcolor("dddddd"))
# the 1px wide grey center line
rect = QRect(round(10), self._handle_width, round(1),
self.height() - 2 * self._handle_width)
painter.drawRoundedRect(rect, 3.0, 3.0)
# the blue line
rect = QRect(round(7),
self._value_to_pixel(self.lower_value.value),
round(6),
self._value_to_pixel(self.upper_value.value - self.lower_value.value) - 1 * self._handle_width)
painter.drawRoundedRect(rect, 3.0, 3.0)
def _draw_handle(self, painter: QPainter, handle: RangeSliderHandle, direction=1) -> None:
y_pixel = self._value_to_pixel(handle.value)
h = self._handle_width - 1 # height of the handle
painter.setBrush(to_qcolor("dddddd"))
painter.setPen(to_qcolor("444444"))
painter.setRenderHint(PySide6.QtGui.QPainter.RenderHint.Antialiasing, False)
painter.drawLine(2, y_pixel, 18, y_pixel)
painter.setRenderHint(PySide6.QtGui.QPainter.RenderHint.Antialiasing, True)
painter.drawPolygon(
(QPoint(10, y_pixel), QPoint(18, y_pixel + h * direction), QPoint(2, y_pixel + h * direction)))
def _value_to_pixel(self, value: int) -> int:
value_percent = value / self.max_value
pixel = (self.height() - 2 * self._handle_width) * value_percent + self._handle_width
return pixel
def _draw_hits(self, painter: QPainter) -> None:
color = to_qcolor("000000")
color.setAlpha(192)
pen = QPen(color)
pen.setWidth(1)
painter.setPen(pen)
# compute where to draw a line and then deduplicate then, so that we don't draw lines multiple times
# this is for performance and because we use transparency and drawing a line multiple times would make it
# darker
paint_at_y_positions: set[int] = set(())
for hit_position in self._hit_positions:
y = (self.height() - 2 * self._handle_width) * hit_position + self._handle_width
y = round(y)
paint_at_y_positions.add(y)
for y in paint_at_y_positions:
painter.drawLine(2, y, 18, y)
def _pixel_to_value(self, pixel: int) -> int:
pixel_percent = (pixel - self._handle_width) / (self.height() - 2 * self._handle_width)
return int(math.floor(self.max_value * pixel_percent))
def _is_on_handle(self, handle: RangeSliderHandle, y_pixel: int, direction=1) -> bool:
handle_y_pixel = self._value_to_pixel(handle.value)
if direction > 0:
return handle_y_pixel < y_pixel < handle_y_pixel + self._handle_width
else:
return handle_y_pixel - self._handle_width < y_pixel < handle_y_pixel
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.NoModifier:
pos: QPoint = e.pos()
if self._is_on_handle(self.lower_value, pos.y(), direction=-1):
self.selected_handle = self.lower_value
self.selection_drag_range = (self.min_value, self.upper_value.value)
self.drag_y_offset_in_handle = self.selected_handle.value - self._pixel_to_value(pos.y())
if self._is_on_handle(self.upper_value, pos.y(), direction=1):
self.selected_handle = self.upper_value
self.selection_drag_range = (self.lower_value.value, self.max_value)
self.drag_y_offset_in_handle = self.selected_handle.value - self._pixel_to_value(pos.y())
def mouseReleaseEvent(self, event: PySide6.QtGui.QMouseEvent) -> None:
self.selected_handle = None
def mouseMoveEvent(self, e: PySide6.QtGui.QMouseEvent) -> None:
if self.selected_handle != None:
pos: QPoint = e.pos()
value = self._pixel_to_value(pos.y()) + self.drag_y_offset_in_handle
if self.selection_drag_range[0] <= value <= self.selection_drag_range[1]:
self.selected_handle.value = value
# print("%s, %s" %(self.lower_value.value, self.upper_value.value))
self._emit_value_changed()
self.update()
self._update_tooltip()
def set_range_start(self, value: int):
self.lower_value.value = value
self._emit_value_changed()
self.update()
def set_range_end(self, value: int):
self.upper_value.value = value
self._emit_value_changed()
self.update()
def _emit_value_changed(self):
# print(f"emit {str(self.lower_value.value)}, {str(self.upper_value.value)}")
self.value_changed.emit(str(self.lower_value.value), str(self.upper_value.value))
def _update_tooltip(self):
text = _("showing bytes {0} to {1} ({2})").format(
self.lower_value.value,
self.upper_value.value,
conversion.humanbytes(self.upper_value.value - self.lower_value.value))
PluginRegistry.execute("update_status_bar", text)
self.setToolTip(text)