Source code for qudi.util.widgets.toggle_switch

# -*- 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)