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)