ayon-core/openpype/widgets/nice_checkbox.py
2023-01-06 18:51:51 +01:00

540 lines
17 KiB
Python

from math import floor, sqrt, ceil
from qtpy import QtWidgets, QtCore, QtGui
from openpype.style import get_objected_colors
class NiceCheckbox(QtWidgets.QFrame):
stateChanged = QtCore.Signal(int)
clicked = QtCore.Signal()
_checked_bg_color = None
_unchecked_bg_color = None
_checker_color = None
_checker_hover_color = None
def __init__(self, checked=False, draw_icons=False, parent=None):
super(NiceCheckbox, self).__init__(parent)
self.setObjectName("NiceCheckbox")
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setSizePolicy(
QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Fixed
)
self._checked = checked
if checked:
checkstate = QtCore.Qt.Checked
else:
checkstate = QtCore.Qt.Unchecked
self._checkstate = checkstate
self._is_tristate = False
self._draw_icons = draw_icons
self._animation_timer = QtCore.QTimer(self)
self._animation_timeout = 6
self._fixed_width_set = False
self._fixed_height_set = False
self._current_step = None
self._steps = 21
self._middle_step = 11
self.set_steps(self._steps)
self._checker_margins_divider = 0
self._pressed = False
self._under_mouse = False
self.icon_scale_factor = sqrt(2) / 2
icon_path_stroker = QtGui.QPainterPathStroker()
icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap)
icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin)
self.icon_path_stroker = icon_path_stroker
self._animation_timer.timeout.connect(self._on_animation_timeout)
self._base_size = QtCore.QSize(90, 50)
self._load_colors()
@classmethod
def _load_colors(cls):
if cls._checked_bg_color is not None:
return
colors_info = get_objected_colors("nice-checkbox")
cls._checked_bg_color = colors_info["bg-checked"].get_qcolor()
cls._unchecked_bg_color = colors_info["bg-unchecked"].get_qcolor()
cls._checker_color = colors_info["bg-checker"].get_qcolor()
cls._checker_hover_color = colors_info["bg-checker-hover"].get_qcolor()
@property
def checked_bg_color(self):
return self._checked_bg_color
@property
def unchecked_bg_color(self):
return self._unchecked_bg_color
@property
def checker_color(self):
return self._checker_color
@property
def checker_hover_color(self):
return self._checker_hover_color
def setTristate(self, tristate=True):
if self._is_tristate != tristate:
self._is_tristate = tristate
def set_draw_icons(self, draw_icons=None):
if draw_icons is None:
draw_icons = not self._draw_icons
if draw_icons == self._draw_icons:
return
self._draw_icons = draw_icons
self.repaint()
def sizeHint(self):
height = self.fontMetrics().height()
width = self.get_width_hint_by_height(height)
return QtCore.QSize(width, height)
def get_width_hint_by_height(self, height):
return int((
float(height) / self._base_size.height()
) * self._base_size.width())
def get_height_hint_by_width(self, width):
return int((
float(width) / self._base_size.width()
) * self._base_size.height())
def setFixedHeight(self, *args, **kwargs):
self._fixed_height_set = True
super(NiceCheckbox, self).setFixedHeight(*args, **kwargs)
if not self._fixed_width_set:
width = self.get_width_hint_by_height(self.height())
self.setFixedWidth(width)
def setFixedWidth(self, *args, **kwargs):
self._fixed_width_set = True
super(NiceCheckbox, self).setFixedWidth(*args, **kwargs)
if not self._fixed_height_set:
height = self.get_height_hint_by_width(self.width())
self.setFixedHeight(height)
def setFixedSize(self, *args, **kwargs):
self._fixed_height_set = True
self._fixed_width_set = True
super(NiceCheckbox, self).setFixedSize(*args, **kwargs)
def steps(self):
return self._steps
def set_steps(self, steps):
if steps < 2:
steps = 2
# Make sure animation is stopped
if self._animation_timer.isActive():
self._animation_timer.stop()
# Set steps and set current step by current checkstate
self._steps = steps
diff = steps % 2
self._middle_step = (int(steps - diff) / 2) + diff
if self._checkstate == QtCore.Qt.Checked:
self._current_step = self._steps
elif self._checkstate == QtCore.Qt.Unchecked:
self._current_step = 0
else:
self._current_step = self._middle_step
def checkState(self):
return self._checkstate
def isChecked(self):
return self._checked
def _checkstate_int_to_enum(self, state):
if not isinstance(state, int):
return state
if state == 2:
return QtCore.Qt.Checked
if state == 1:
return QtCore.Qt.PartiallyChecked
return QtCore.Qt.Unchecked
def _checkstate_enum_to_int(self, state):
if isinstance(state, int):
return state
if state == QtCore.Qt.Checked:
return 2
if state == QtCore.Qt.PartiallyChecked:
return 1
return 0
def setCheckState(self, state):
state = self._checkstate_int_to_enum(state)
if self._checkstate == state:
return
self._checkstate = state
if state == QtCore.Qt.Checked:
self._checked = True
elif state == QtCore.Qt.Unchecked:
self._checked = False
self.stateChanged.emit(self._checkstate_enum_to_int(self.checkState()))
if self._animation_timer.isActive():
self._animation_timer.stop()
if self.isVisible() and self.isEnabled():
# Start animation
self._animation_timer.start(self._animation_timeout)
else:
# Do not animate change if is disabled
if state == QtCore.Qt.Checked:
self._current_step = self._steps
elif state == QtCore.Qt.Unchecked:
self._current_step = 0
else:
self._current_step = self._middle_step
self.repaint()
def setChecked(self, checked):
if checked == self._checked:
return
if checked:
checkstate = QtCore.Qt.Checked
else:
checkstate = QtCore.Qt.Unchecked
self.setCheckState(checkstate)
def nextCheckState(self):
if self._checkstate == QtCore.Qt.Unchecked:
if self._is_tristate:
return QtCore.Qt.PartiallyChecked
return QtCore.Qt.Checked
if self._checkstate == QtCore.Qt.Checked:
return QtCore.Qt.Unchecked
if self._checked:
return QtCore.Qt.Unchecked
return QtCore.Qt.Checked
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self._pressed = True
self.repaint()
super(NiceCheckbox, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if self._pressed and not event.buttons() & QtCore.Qt.LeftButton:
self._pressed = False
if self.rect().contains(event.pos()):
self.setCheckState(self.nextCheckState())
self.clicked.emit()
event.accept()
return
super(NiceCheckbox, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
if self._pressed:
under_mouse = self.rect().contains(event.pos())
if under_mouse != self._under_mouse:
self._under_mouse = under_mouse
self.repaint()
super(NiceCheckbox, self).mouseMoveEvent(event)
def enterEvent(self, event):
self._under_mouse = True
if self.isEnabled():
self.repaint()
super(NiceCheckbox, self).enterEvent(event)
def leaveEvent(self, event):
self._under_mouse = False
if self.isEnabled():
self.repaint()
super(NiceCheckbox, self).leaveEvent(event)
def _on_animation_timeout(self):
if self._checkstate == QtCore.Qt.Checked:
if self._current_step == self._steps:
self._animation_timer.stop()
return
self._current_step += 1
elif self._checkstate == QtCore.Qt.Unchecked:
if self._current_step == 0:
self._animation_timer.stop()
return
self._current_step -= 1
else:
if self._current_step < self._middle_step:
self._current_step += 1
elif self._current_step > self._middle_step:
self._current_step -= 1
if self._current_step == self._middle_step:
self._animation_timer.stop()
self.repaint()
@staticmethod
def steped_color(color1, color2, offset_ratio):
red_dif = (
color1.red() - color2.red()
)
green_dif = (
color1.green() - color2.green()
)
blue_dif = (
color1.blue() - color2.blue()
)
red = int(color2.red() + (
red_dif * offset_ratio
))
green = int(color2.green() + (
green_dif * offset_ratio
))
blue = int(color2.blue() + (
blue_dif * offset_ratio
))
return QtGui.QColor(red, green, blue)
def paintEvent(self, event):
frame_rect = QtCore.QRect(self.rect())
if frame_rect.width() < 0 or frame_rect.height() < 0:
return
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
# Draw inner background
if self._current_step == self._steps:
bg_color = self.checked_bg_color
elif self._current_step == 0:
bg_color = self.unchecked_bg_color
else:
offset_ratio = float(self._current_step) / self._steps
# Animation bg
bg_color = self.steped_color(
self.checked_bg_color,
self.unchecked_bg_color,
offset_ratio
)
margins_ratio = float(self._checker_margins_divider)
if margins_ratio > 0:
size_without_margins = int(
(float(frame_rect.height()) / margins_ratio)
* (margins_ratio - 2)
)
size_without_margins -= size_without_margins % 2
margin_size_c = ceil(
frame_rect.height() - size_without_margins
) / 2
else:
size_without_margins = frame_rect.height()
margin_size_c = 0
checkbox_rect = QtCore.QRect(
frame_rect.x() + margin_size_c,
frame_rect.y() + margin_size_c,
frame_rect.width() - (margin_size_c * 2),
frame_rect.height() - (margin_size_c * 2)
)
if checkbox_rect.width() > checkbox_rect.height():
radius = floor(checkbox_rect.height() * 0.5)
else:
radius = floor(checkbox_rect.width() * 0.5)
painter.setPen(QtCore.Qt.transparent)
painter.setBrush(bg_color)
painter.drawRoundedRect(checkbox_rect, radius, radius)
# Draw checker
checker_size = size_without_margins - (margin_size_c * 2)
area_width = (
checkbox_rect.width()
- (margin_size_c * 2)
- checker_size
)
if self._current_step == 0:
x_offset = 0
else:
x_offset = (float(area_width) / self._steps) * self._current_step
pos_x = checkbox_rect.x() + x_offset + margin_size_c
pos_y = checkbox_rect.y() + margin_size_c
checker_rect = QtCore.QRect(pos_x, pos_y, checker_size, checker_size)
under_mouse = self.isEnabled() and self._under_mouse
if under_mouse:
checker_color = self.checker_hover_color
else:
checker_color = self.checker_color
painter.setBrush(checker_color)
painter.drawEllipse(checker_rect)
if self._draw_icons:
painter.setBrush(bg_color)
icon_path = self._get_icon_path(painter, checker_rect)
painter.drawPath(icon_path)
# Draw shadow overlay
if not self.isEnabled():
level = 33
alpha = 127
painter.setPen(QtCore.Qt.transparent)
painter.setBrush(QtGui.QColor(level, level, level, alpha))
painter.drawRoundedRect(checkbox_rect, radius, radius)
painter.end()
def _get_icon_path(self, painter, checker_rect):
self.icon_path_stroker.setWidth(checker_rect.height() / 5)
if self._current_step == self._steps:
return self._get_enabled_icon_path(painter, checker_rect)
if self._current_step == 0:
return self._get_disabled_icon_path(painter, checker_rect)
if self._current_step == self._middle_step:
return self._get_middle_circle_path(painter, checker_rect)
disabled_step = self._steps - self._current_step
enabled_step = self._steps - disabled_step
half_steps = self._steps + 1 - ((self._steps + 1) % 2)
if enabled_step > disabled_step:
return self._get_enabled_icon_path(
painter, checker_rect, enabled_step, half_steps
)
else:
return self._get_disabled_icon_path(
painter, checker_rect, disabled_step, half_steps
)
def _get_middle_circle_path(self, painter, checker_rect):
width = self.icon_path_stroker.width()
path = QtGui.QPainterPath()
path.addEllipse(checker_rect.center(), width, width)
return path
def _get_enabled_icon_path(
self, painter, checker_rect, step=None, half_steps=None
):
fifteenth = float(checker_rect.height()) / 15
# Left point
p1 = QtCore.QPoint(
int(checker_rect.x() + (5 * fifteenth)),
int(checker_rect.y() + (9 * fifteenth))
)
# Middle bottom point
p2 = QtCore.QPoint(
checker_rect.center().x(),
int(checker_rect.y() + (11 * fifteenth))
)
# Top right point
p3 = QtCore.QPoint(
int(checker_rect.x() + (10 * fifteenth)),
int(checker_rect.y() + (5 * fifteenth))
)
if step is not None:
multiplier = (half_steps - step)
p1c = p1 - checker_rect.center()
p2c = p2 - checker_rect.center()
p3c = p3 - checker_rect.center()
p1o = QtCore.QPoint(
int((float(p1c.x()) / half_steps) * multiplier),
int((float(p1c.y()) / half_steps) * multiplier)
)
p2o = QtCore.QPoint(
int((float(p2c.x()) / half_steps) * multiplier),
int((float(p2c.y()) / half_steps) * multiplier)
)
p3o = QtCore.QPoint(
int((float(p3c.x()) / half_steps) * multiplier),
int((float(p3c.y()) / half_steps) * multiplier)
)
p1 -= p1o
p2 -= p2o
p3 -= p3o
path = QtGui.QPainterPath(p1)
path.lineTo(p2)
path.lineTo(p3)
return self.icon_path_stroker.createStroke(path)
def _get_disabled_icon_path(
self, painter, checker_rect, step=None, half_steps=None
):
center_point = QtCore.QPointF(
float(checker_rect.width()) / 2,
float(checker_rect.height()) / 2
)
offset = float((
(center_point + QtCore.QPointF(0, 0)) / 2
).x()) / 4 * 5
if step is not None:
diff = center_point.x() - offset
diff_offset = (diff / half_steps) * (half_steps - step)
offset += diff_offset
line1_p1 = QtCore.QPointF(
checker_rect.topLeft().x() + offset,
checker_rect.topLeft().y() + offset,
)
line1_p2 = QtCore.QPointF(
checker_rect.bottomRight().x() - offset,
checker_rect.bottomRight().y() - offset
)
line2_p1 = QtCore.QPointF(
checker_rect.bottomLeft().x() + offset,
checker_rect.bottomLeft().y() - offset
)
line2_p2 = QtCore.QPointF(
checker_rect.topRight().x() - offset,
checker_rect.topRight().y() + offset
)
path = QtGui.QPainterPath()
path.moveTo(line1_p1)
path.lineTo(line1_p2)
path.moveTo(line2_p1)
path.lineTo(line2_p2)
return self.icon_path_stroker.createStroke(path)