194 lines
5.9 KiB
Python
Raw Normal View History

from PyQt6.QtCore import QRect, QRectF, QSize, Qt, pyqtSignal
from PyQt6.QtGui import QColor, QLinearGradient, QPainter, QPen
from PyQt6.QtWidgets import QColorDialog, QSizePolicy, QWidget
class Gradient(QWidget):
gradientChanged = pyqtSignal()
def __init__(self, gradient=None):
super().__init__()
self.setSizePolicy(
QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding
)
if gradient:
self._gradient = gradient
else:
self._gradient = [
(0.0, "#000000"),
(1.0, "#ffffff"),
]
# Stop point handle sizes.
self._handle_w = 10
self._handle_h = 10
self._drag_position = None
def paintEvent(self, e):
painter = QPainter(self)
width = painter.device().width()
height = painter.device().height()
# Draw the linear horizontal gradient.
gradient = QLinearGradient(0, 0, width, 0)
for stop, color in self._gradient:
gradient.setColorAt(stop, QColor(color))
rect = QRect(0, 0, width, height)
painter.fillRect(rect, gradient)
pen = QPen()
y = painter.device().height() / 2
# Draw the stop handles.
for stop, _ in self._gradient:
pen.setColor(QColor("white"))
painter.setPen(pen)
painter.drawLine(
int(stop * width),
int(y - self._handle_h),
int(stop * width),
int(y + self._handle_h),
)
pen.setColor(QColor("red"))
painter.setPen(pen)
rect = QRectF(
stop * width - self._handle_w / 2,
y - self._handle_h / 2,
self._handle_w,
self._handle_h,
)
painter.drawRect(rect)
painter.end()
def sizeHint(self):
return QSize(200, 50)
def _sort_gradient(self):
self._gradient = sorted(self._gradient, key=lambda g: g[0])
def _constrain_gradient(self):
self._gradient = [
# Ensure values within valid range.
(max(0.0, min(1.0, stop)), color)
for stop, color in self._gradient
]
def setGradient(self, gradient):
assert all([0.0 <= stop <= 1.0 for stop, _ in gradient])
self._gradient = gradient
self._constrain_gradient()
self._sort_gradient()
self.gradientChanged.emit()
def gradient(self):
return self._gradient
@property
def _end_stops(self):
return [0, len(self._gradient) - 1]
def addStop(self, stop, color=None):
# Stop is a value 0...1, find the point to insert this stop
# in the list.
assert 0.0 <= stop <= 1.0
for n, g in enumerate(self._gradient):
if g[0] > stop:
# Insert before this entry, with specified or next color.
self._gradient.insert(n, (stop, color or g[1]))
break
self._constrain_gradient()
self.gradientChanged.emit()
self.update()
def removeStopAtPosition(self, n):
if n not in self._end_stops:
del self._gradient[n]
self.gradientChanged.emit()
self.update()
def setColorAtPosition(self, n, color):
if n < len(self._gradient):
stop, _ = self._gradient[n]
self._gradient[n] = stop, color
self.gradientChanged.emit()
self.update()
def chooseColorAtPosition(self, n, current_color=None):
dlg = QColorDialog(self)
if current_color:
dlg.setCurrentColor(QColor(current_color))
if dlg.exec():
self.setColorAtPosition(n, dlg.currentColor().name())
def _find_stop_handle_for_event(self, e, to_exclude=None):
width = self.width()
height = self.height()
midpoint = height / 2
# Are we inside a stop point? First check y.
if (
e.position().y() >= midpoint - self._handle_h
and e.position().y() <= midpoint + self._handle_h
):
for n, (stop, color) in enumerate(self._gradient):
if to_exclude and n in to_exclude:
# Allow us to skip the extreme ends of the gradient.
continue
if (
e.position().x() >= stop * width - self._handle_w
and e.position().x() <= stop * width + self._handle_w
):
return n
def mousePressEvent(self, e):
# We're in this stop point.
if e.button() == Qt.MouseButton.RightButton:
n = self._find_stop_handle_for_event(e)
if n is not None:
_, color = self._gradient[n]
self.chooseColorAtPosition(n, color)
elif e.button() == Qt.MouseButton.LeftButton:
n = self._find_stop_handle_for_event(e, to_exclude=self._end_stops)
if n is not None:
# Activate drag mode.
self._drag_position = n
def mouseReleaseEvent(self, e):
self._drag_position = None
self._sort_gradient()
def mouseMoveEvent(self, e):
# If drag active, move the stop.
if self._drag_position:
stop = e.position().x() / self.width()
_, color = self._gradient[self._drag_position]
self._gradient[self._drag_position] = stop, color
self._constrain_gradient()
self.update()
def mouseDoubleClickEvent(self, e):
# Calculate the position of the click relative 0..1 to the width.
n = self._find_stop_handle_for_event(e)
if n:
self._sort_gradient() # Ensure ordered.
# Delete existing, if not at the ends.
if n > 0 and n < len(self._gradient) - 1:
self.removeStopAtPosition(n)
else:
stop = e.position().x() / self.width()
self.addStop(stop)