# -*- coding: utf-8 -*-
"""
This file contains a touch-like toggle switch.
Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
distribution and on <https://github.com/Ulm-IQO/qudi-core/>
This file is part of qudi.
Qudi is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with qudi.
If not, see <https://www.gnu.org/licenses/>.
"""
__all__ = ['ToggleSwitch']
from PySide2 import QtWidgets, QtCore, QtGui
[docs]
class ToggleSwitch(QtWidgets.QAbstractButton):
""" A mobile/touch inspired toggle switch to switch between two states.
"""
sigStateChanged = QtCore.Signal(str)
[docs]
def __init__(self, parent=None, state_names=None, thumb_track_ratio=1, scale_text=True,
display_text=True):
super().__init__(parent=parent)
self.setCheckable(True)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
# check state_names
if state_names is None:
self._state_names = None
elif len(state_names) != 2 or not all(isinstance(n, str) and n != '' for n in state_names):
raise ValueError(
f'state_names must be None or sequence of exactly 2 non-empty strings. '
f'Received: {state_names}'
)
else:
self._state_names = tuple(state_names)
# check thumb_track_ratio
if thumb_track_ratio <= 0:
raise ValueError(
f'thumb_track_ratio must have a value > 0. Received: {thumb_track_ratio}'
)
self._thumb_track_ratio = thumb_track_ratio
# check scale_text
if not isinstance(scale_text, bool):
raise ValueError(f'scale_text must be bool type. Received: {scale_text}')
self._scale_text = scale_text
# check display_text
if not isinstance(display_text, bool):
raise ValueError(f'display_text must be bool type. Received: {display_text}')
self._display_text = display_text if self._state_names else False
self.__display_text = self._display_text
# Determine (minimal) size hint based on text to display
self._default_text_size = None
self._size_hint = None
self._refresh_size_hint()
# Calculate geometry for painting
self._thumb_radius = 0
self._track_radius = 0
self._track_margin = 0
self._thumb_origin = 0
self._current_text_width = 0
self._refresh_geometry()
self._refresh_text_scale()
# Determine appearance from current palette depending on thumb style
palette = self.palette()
if self._thumb_track_ratio > 1:
self._track_colors = (palette.dark(), palette.highlight())
self._thumb_colors = (palette.light(), palette.highlight())
self._text_colors = (palette.text().color(), palette.highlightedText().color())
self._track_opacity = 0.5
else:
self._track_colors = (palette.dark(), palette.highlight())
self._thumb_colors = (palette.light(), palette.highlightedText())
self._text_colors = (palette.text().color(), palette.highlightedText().color())
self._track_opacity = 1
# property value for current thumb position
self._thumb_position = self._thumb_origin
# Connect notifier signal
self.clicked.connect(self._notify_state_change)
# set up the animation
self._slider_animation = QtCore.QPropertyAnimation(self, b'thumb_position', self)
self._slider_animation.finished.connect(self._finish_animation)
@QtCore.Slot()
def _finish_animation(self):
target_position = self._thumb_end
if self._thumb_position != target_position:
self.thumb_position = target_position
def _refresh_size_hint(self):
metrics = QtGui.QFontMetrics(self.font())
if self._display_text:
self._default_text_size = QtCore.QSize(
max(metrics.horizontalAdvance(f' {text} ') for text in self._state_names),
metrics.height()
)
else:
self._default_text_size = QtCore.QSize(metrics.horizontalAdvance(' OFF '),
metrics.height())
if self._thumb_track_ratio <= 1:
height = self._default_text_size.height() * 1.5
else:
height = self._default_text_size.height() * 1.5 * self._thumb_track_ratio
width = self._default_text_size.width() + 2 * height
self._size_hint = QtCore.QSize(width, height)
self.setMinimumSize(self._size_hint)
def _refresh_text_scale(self):
if not self._display_text:
self._current_text_width = 0
self.__display_text = False
return
if self._scale_text:
# Determine current maximum height and width for text field
max_height = int(round(1.5 * self._track_radius))
if self._thumb_track_ratio > 1:
max_width = int(round(self.width() - 4 * self._thumb_radius))
else:
max_width = int(round(self.width() - 4 * self._track_radius))
# Return early if there is simply no space between thumb positions
if max_width <= 0:
self._current_text_width = 0
self.__display_text = False
return
font = self.font()
font.setPixelSize(max_height)
metrics = QtGui.QFontMetrics(font)
text_width = max(
metrics.horizontalAdvance(f' {text} ') for text in self._state_names if text
)
if text_width > max_width:
text_scale = max_width / text_width
font.setPixelSize(max(1, int(round(max_height * text_scale))))
super().setFont(font)
metrics = QtGui.QFontMetrics(self.font())
self._current_text_width = max(
metrics.horizontalAdvance(f' {text} ') for text in self._state_names
)
self.__display_text = True
def _refresh_geometry(self):
# Calculate new size for track and thumb
height = self.height()
if self._thumb_track_ratio > 1:
self._thumb_radius = height / 2
self._track_radius = self._thumb_radius / self._thumb_track_ratio
else:
self._track_radius = height / 2
self._thumb_radius = self._track_radius * self._thumb_track_ratio
self._track_margin = max(0.0, self._thumb_radius - self._track_radius)
self._thumb_origin = max(self._thumb_radius, self._track_radius)
def setFont(self, new_font):
super().setFont(new_font)
self._refresh_size_hint()
self._refresh_geometry()
self._refresh_text_scale()
self.update()
@QtCore.Slot()
def _notify_state_change(self):
state = self.current_state
self.sigStateChanged.emit(state if isinstance(state, str) else '')
@property
def current_state(self):
is_checked = self.isChecked()
return self._state_names[int(is_checked)] if self._state_names else is_checked
@property
def _thumb_end(self):
return self.width() - self._thumb_origin if self.isChecked() else self._thumb_origin
@property
def _track_color(self):
return self._track_colors[int(self.isChecked())]
@property
def _thumb_color(self):
return self._thumb_colors[int(self.isChecked())]
@property
def _text_color(self):
return self._text_colors[int(self.isChecked())]
@QtCore.Property(float)
def thumb_position(self):
return self._thumb_position
@thumb_position.setter
def thumb_position(self, value):
self._thumb_position = value
self.update()
def sizeHint(self):
return self._size_hint
def minimumSizeHint(self):
return self._size_hint
def setChecked(self, checked):
super().setChecked(checked)
self._thumb_position = self._thumb_end
def resizeEvent(self, event):
self._refresh_geometry()
self._refresh_text_scale()
self.thumb_position = self._thumb_end
event.accept()
def paintEvent(self, event):
# Set up painter
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing, True)
p.setPen(QtCore.Qt.NoPen)
track_opacity = self._track_opacity
if self.isEnabled():
track_brush = self._track_color
thumb_brush = self._thumb_color
text_color = self._text_color
else:
palette = self.palette()
track_opacity *= 0.8
track_brush = palette.shadow()
thumb_brush = palette.mid()
text_color = palette.shadow().color()
# draw track
p.setPen(QtCore.Qt.NoPen)
p.setBrush(track_brush)
p.setOpacity(track_opacity)
p.drawRoundedRect(self._track_margin,
max(0, self.height() / 2 - self._track_radius),
max(0, self.width() - 2 * self._track_margin),
2 * self._track_radius,
self._track_radius,
self._track_radius)
# draw text if necessary
if self.__display_text and self._current_text_width > 0:
p.setPen(text_color)
p.setOpacity(1.0)
p.setFont(self.font())
p.drawText(self._track_margin,
self.height() / 2 - self._track_radius,
self.width() - 2 * self._track_margin,
2 * self._track_radius,
QtCore.Qt.AlignCenter,
self.current_state)
# draw thumb
p.setPen(QtCore.Qt.NoPen)
p.setBrush(thumb_brush)
p.setOpacity(1.0)
p.drawEllipse(self._thumb_position - self._thumb_radius,
int(round(self.height()/2 - self._thumb_radius)),
2 * self._thumb_radius,
2 * self._thumb_radius)
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
if event.button() == QtCore.Qt.LeftButton:
self._slider_animation.setDuration(200)
self._slider_animation.setStartValue(self._thumb_position)
self._slider_animation.setEndValue(self._thumb_end)
self._slider_animation.start()
def enterEvent(self, event):
self.setCursor(QtCore.Qt.PointingHandCursor)
super().enterEvent(event)