diff --git a/src/ui/colorbutton.py b/src/ui/colorbutton.py index 725d8ec..8cf8186 100644 --- a/src/ui/colorbutton.py +++ b/src/ui/colorbutton.py @@ -3,6 +3,8 @@ import re from PySide6.QtGui import QColor, QPixmap, QIcon from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QColorDialog, QSizePolicy, QComboBox from src.i18n import _ +from src.util.color import to_qcolor + class ColorButton(QWidget): def __init__(self, color: str, parent=None): @@ -69,7 +71,7 @@ class ColorButton(QWidget): self.color_drop_down.setCurrentIndex(self.color_drop_down.count() - 1) def _update_color(self): - new_color = QColorDialog.getColor(self._to_qcolor(self.color)) + new_color = QColorDialog.getColor(to_qcolor(self.color)) if new_color.isValid(): color = self._to_hex(new_color) self.set_color(color) @@ -80,20 +82,10 @@ class ColorButton(QWidget): def _color_pixmap(self, color: str) -> QPixmap: pixmap = QPixmap(40, 40) - qcolor = self._to_qcolor(color) + qcolor = to_qcolor(color) pixmap.fill((qcolor)) return pixmap - def _to_qcolor(self, color: str): - if self._is_hex_color(color): - red = int(color[0:2], 16) - green = int(color[2:4], 16) - blue = int(color[4:6], 16) - return QColor(red, green, blue) - elif color in QColor().colorNames(): - return QColor(color) - return QColor(255, 255, 255) - def _to_hex(self, color: QColor) -> str: red = "{0:0{1}x}".format(color.red(), 2) green = "{0:0{1}x}".format(color.green(), 2) diff --git a/src/ui/rangeslider.py b/src/ui/rangeslider.py new file mode 100644 index 0000000..9b2e403 --- /dev/null +++ b/src/ui/rangeslider.py @@ -0,0 +1,154 @@ +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 +from PySide6.QtWidgets import QWidget + +from src.util.color import to_qcolor + + +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(float, float) + + _width = 20 + _handle_width = 12 + + def __init__(self): + super(RangeSlider, self).__init__() + self.setFixedWidth(self._width) + + self.draw_ticks = False + + self.min_value = 0 + self.max_value = 100 + + self.lower_value = RangeSliderHandle(0) + self.upper_value = RangeSliderHandle(100) + + self.selected_handle = None + self.selection_drag_range = (self.min_value, self.max_value) + + def set_max_value(self, max_value: int, update_style=UpdateStyle.KEEP_ABSOLUTE_MIN_MAX): + old_max_value = self.max_value + self.max_value = max_value + + if update_style == UpdateStyle.KEEP_ABSOLUTE_MIN_MAX: + if self.upper_value.value == old_max_value: + self.upper_value.value = self.max_value + + self.lower_value.value = min(self.lower_value.value, max_value) + self.upper_value.value = min(self.upper_value.value, max_value) + self.update() + + def paintEvent(self, event: PySide6.QtGui.QPaintEvent) -> None: + painter = QPainter(self) + self._draw_background(painter) + if self.draw_ticks: + self._draw_ticks(painter) + self._draw_handle(painter, self.lower_value) + self._draw_handle(painter, self.upper_value, direction=-1) + + painter.end() + + def _draw_background(self, painter: QPainter) -> None: + painter.setBrush(to_qcolor("50ade8")) + painter.setPen(to_qcolor("dddddd")) + + rect = QRect(round(10), self._handle_width, round(1), + self.height() - 2 * self._handle_width) + painter.drawRoundedRect(rect, 3.0, 3.0) + + rect = QRect(round(7), + self._value_to_pixel(self.lower_value.value) + self._handle_width, + round(6), + self._value_to_pixel(self.upper_value.value - self.lower_value.value) - 2 * self._handle_width) + painter.drawRoundedRect(rect, 3.0, 3.0) + + def _draw_ticks(self, painter: QPainter) -> None: + painter.setPen(to_qcolor("333333")) + + min_tick_distance = 25 + full_height = self.height() - 2 * self._handle_width + ticks = math.floor(full_height / min_tick_distance) + actual_tick_distance = full_height / ticks + print(f"ticks {ticks}") + y = actual_tick_distance + self._handle_width + while y < full_height: + painter.drawLine(8, y, 12, y) + y = y + actual_tick_distance + + def _draw_handle(self, painter: QPainter, handle: RangeSliderHandle, direction=1) -> None: + y_pixel = self._value_to_pixel(handle.value) + + painter.setBrush(to_qcolor("dddddd")) + painter.setPen(to_qcolor("444444")) + painter.setRenderHint(PySide6.QtGui.QPainter.RenderHint.Antialiasing, True) + painter.drawLine(2, y_pixel, 18, y_pixel) + painter.drawPolygon( + (QPoint(10, y_pixel), QPoint(18, y_pixel + 12 * direction), QPoint(2, y_pixel + 12 * direction))) + + def _draw_handle_circle(self, painter: QPainter, handle: RangeSliderHandle) -> None: + y_pixel = self._value_to_pixel(handle.value) + + painter.setBrush(to_qcolor("dddddd")) + painter.setPen(to_qcolor("444444")) + painter.setRenderHint(PySide6.QtGui.QPainter.RenderHint.Antialiasing, True) + painter.drawEllipse(QPoint(self._width / 2, y_pixel), self._handle_width / 2 - 1, self._handle_width / 2 - 1) + + def _value_to_pixel(self, value: int) -> int: + value_percent = value / (self.max_value - self.min_value) + pixel = (self.height() - 2 * self._handle_width) * value_percent + self._handle_width + return pixel + + def _pixel_to_value(self, pixel: int) -> int: + pixel_percent = (pixel - self._handle_width) / (self.height() - 2 * self._handle_width) + return (self.max_value - self.min_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) + 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) + + 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()) + 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.value_changed.emit(self.lower_value.value, self.upper_value.value) + self.update() diff --git a/src/util/color.py b/src/util/color.py new file mode 100644 index 0000000..b97f101 --- /dev/null +++ b/src/util/color.py @@ -0,0 +1,18 @@ +import re + +from PySide6.QtGui import QColor + + +def is_hex_color(color: str): + return re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE) + + +def to_qcolor(color: str): + if is_hex_color(color): + red = int(color[0:2], 16) + green = int(color[2:4], 16) + blue = int(color[4:6], 16) + return QColor(red, green, blue) + elif color in QColor().colorNames(): + return QColor(color) + return QColor(255, 255, 255)