def fade_animation(start=0, end=1, duration=300, object=None, on_finished=None): """ Fade animation for widgets :param start: int, animation start value :param end: int, animation end value :param duration: int, duration of the effect :param object: variant, QDialog || QMainWindow :param on_finished: variant, function to call when the animation is finished :return: QPropertyAnimation """ anim_curve = QEasingCurve() anim_curve.setType(QEasingCurve.OutQuint) if start == 'current': start = object.opacity() if end == 'current': end = object.opacity() animation = QPropertyAnimation(object, b'opacity', object) animation.setEasingCurve(anim_curve) animation.setDuration(duration) animation.setStartValue(start) animation.setEndValue(end) animation.start() if on_finished: animation.finished.connect(on_finished)
def slide_window(start=-100, end=0, duration=300, object=None, on_finished=None): """ Slide animation for windows :param start: int, animation start value :param end: int, animation end value :param duration: int, duration of the effect :param object: variant, QDialog || QMainWindow :param on_finished: variant, function to call when the animation is finished :return: QPropertyAnimation """ pos = object.pos() animation = QPropertyAnimation(object, b'pos', object) animation.setDuration(duration) anim_curve = QEasingCurve() if start >= end: anim_curve.setType(QEasingCurve.OutExpo) else: anim_curve.setType(QEasingCurve.InOutExpo) animation.setEasingCurve(anim_curve) animation.setStartValue(QPoint(pos.x(), pos.y() + start)) animation.setEndValue(QPoint(pos.x(), pos.y() + end)) animation.start() if on_finished: animation.finished.connect(on_finished) return animation
def property_animation(start=[0, 0], end=[30, 0], duration=300, object=None, property='iconSize', on_finished=None): """ Functions that returns a ready to use QPropertyAnimation :param start: int, animation start value :param end: int, animation end value :param duration: int, duration of the effect :param object: variant, QDialog || QMainWindow :param property: str, object property to animate :param on_finished: variant, function to call when the animation is finished :return: QPropertyAnimation """ animation = QPropertyAnimation(object, property, object) anim_curve = QEasingCurve() anim_curve.setType(QEasingCurve.InOutQuint) animation.setEasingCurve(anim_curve) animation.setDuration(duration) animation.setStartValue(start) animation.setEndValue(end) animation.start() return animation
def fade_out_widget(widget, duration=200, on_finished=None): """ Fade out animation effect for widgets :param widget: QWidget, widget to apply effect :param duration: int, duration of the effect :param on_finished: variant, function to call when the animation is finished :return: QPropertyAnimation """ effect = QGraphicsOpacityEffect(widget) widget.setGraphicsEffect(effect) animation = QPropertyAnimation(effect, b'opacity') animation.setDuration(duration) animation.setStartValue(1.0) animation.setEndValue(0.0) animation.setEasingCurve(QEasingCurve.InOutCubic) animation.start() if on_finished: animation.finished.connect(on_finished) widget._fade_out_ = animation return animation
class CircleLoading(base.BaseWidget, object): def __init__(self, size=None, color=None, speed=1, parent=None): super(CircleLoading, self).__init__(parent=parent) size = size or self.theme_default_size() self.setFixedSize(QSize(size, size)) self._rotation = 0 self._loading_pixmap = resources.pixmap( 'loading', extension='svg', color=color or self.accent_color()).scaledToWidth(size, Qt.SmoothTransformation) self._loading_anim = QPropertyAnimation() self._loading_anim.setTargetObject(self) self._loading_anim.setDuration(1000 * (1 / speed)) self._loading_anim.setPropertyName('rotation') self._loading_anim.setStartValue(0) self._loading_anim.setEndValue(360) self._loading_anim.setLoopCount(-1) self._loading_anim.start() # ============================================================================================================ # PROPERTIES # ============================================================================================================ def _set_rotation(self, value): self._rotation = value self.update() def _get_rotation(self): return self._rotation rotation = Property(int, _get_rotation, _set_rotation) # ============================================================================================================ # OVERRIDES # ============================================================================================================ def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.SmoothPixmapTransform) painter.translate(self._loading_pixmap.width() / 2, self._loading_pixmap.height() / 2) painter.rotate(self._rotation) painter.drawPixmap(-self._loading_pixmap.width() / 2, -self._loading_pixmap.height() / 2, self._loading_pixmap.width(), self._loading_pixmap.height(), self._loading_pixmap) painter.end() return super(CircleLoading, self).paintEvent(event) # ============================================================================================================ # BASE # ============================================================================================================ def set_size(self, size): """ Sets the size of the widget :param size: int """ self.setFixedSize(QSize(size, size)) self._loading_pixmap = self._loading_pixmap.scaledToWidth( size, Qt.SmoothTransformation) @classmethod def tiny(cls, color=None, parent=None): """ Creates a circle loading widget with tiny size :param color: :param parent: :return: """ loading_widget = cls(color=color, parent=parent) loading_theme = loading_widget.theme() loading_size = loading_theme.tiny if loading_theme else theme.Theme.Sizes.TINY loading_widget.set_size(loading_size) return loading_widget @classmethod def small(cls, color=None, parent=None): """ Creates a circle loading widget with small size :param color: :param parent: :return: """ loading_widget = cls(color=color) loading_theme = loading_widget.theme() loading_size = loading_theme.small if loading_theme else theme.Theme.Sizes.SMALL loading_widget.set_size(loading_size) return loading_widget @classmethod def medium(cls, color=None, parent=None): """ Creates a circle loading widget with medium size :param color: :param parent: :return: """ loading_widget = cls(color=color) loading_theme = loading_widget.theme() loading_size = loading_theme.medium if loading_theme else theme.Theme.Sizes.MEDIUM loading_widget.set_size(loading_size) return loading_widget @classmethod def large(cls, color=None, parent=None): """ Creates a circle loading widget with large size :param color: :param parent: :return: """ loading_widget = cls(color=color) loading_theme = loading_widget.theme() loading_size = loading_theme.large if loading_theme else theme.Theme.Sizes.LARGE loading_widget.set_size(loading_size) return loading_widget @classmethod def huge(cls, color=None, parent=None): """ Creates a circle loading widget with huge size :param color: :param parent: :return: """ loading_widget = cls(color=color) loading_theme = loading_widget.theme() loading_size = loading_theme.huge if loading_theme else theme.Theme.Sizes.HUGE loading_widget.set_size(loading_size) return loading_widget
class OpacityEffect(QGraphicsEffect, object): def __init__(self, target, duration, parent=None): super(OpacityEffect, self).__init__(parent=parent) self._target = target self._duration = duration self._animation = QPropertyAnimation(self._target, "opacity") self._animation.setStartValue(0.0) self._animation.setEndValue(0.0) def get_duration(self): return self._duration def set_duration(self, value): self._duration = value def get_target(self): return self._target def set_target(self, value): self._target = value def get_animation(self): return self._animation duration = property(get_duration, set_duration) target = property(get_target, set_target) animation = property(get_animation) def fade_in_out(self): """ Executes the animation """ self._animation.stop() self._animation.setDuration(self._duration) self._animation.setEasingCurve(QEasingCurve.InOutQuad) self._animation.setStartValue(0.0) self._animation.setEndValue(0.0) self._animation.setKeyValueAt(0.3, 1.0) self._animation.setKeyValueAt(0.6, 1.0) self._animation.start() # region Functions def fade_in(self): """ Fade in the opacity property of the effect target """ self._animation.stop() self._animation.setEasingCurve(QEasingCurve.InOutQuad) self._animation.setDuration(self._duration) self._animation.setStartValue(0) self._animation.setEndValue(1) self._animation.start() def fade_out(self): """ Fade out the opacity property of the effect target """ self._animation.stop() self._animation.setEasingCurve(QEasingCurve.InOutQuad) self._animation.setDuration(self._duration) self._animation.setStartValue(1) self._animation.setEndValue(0) self._animation.start()
class BaseToast(base.BaseWidget, object): class ToastTypes(object): INFO = 'info' SUCCESS = 'success' WARNING = 'warning' ERROR = 'error' LOADING = 'loading' DEFAULT_CONFIG = {'duration': 2} toastClosed = Signal() def __init__(self, text, duration=None, toast_type=None, parent=None): self._text = text self._duration = duration self._toast_type = toast_type self._parent = parent super(BaseToast, self).__init__(parent=parent) def get_main_layout(self): main_layout = layouts.VerticalLayout(margins=(0, 0, 0, 0)) return main_layout def ui(self): super(BaseToast, self).ui() self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog | Qt.WA_TranslucentBackground | Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_StyledBackground) self.setFixedSize(QSize(120, 120)) icon_layout = layouts.HorizontalLayout() icon_layout.addStretch() widget_theme = self.theme() if self._toast_type == self.ToastTypes.LOADING: icon_layout.addWidget( loading.CircleLoading(size=widget_theme.huge, color=widget_theme.text_color_inverse)) else: icon_label = avatar.Avatar() icon_label.theme_size = 60 icon_label.image = resources.pixmap( self._toast_type or self.ToastTypes.INFO, color=widget_theme.text_color_inverse) icon_layout.addWidget(icon_label) icon_layout.addStretch() content_label = label.BaseLabel() content_label.setText(self._text or '') content_label.setAlignment(Qt.AlignCenter) self.main_layout.addStretch() self.main_layout.addLayout(icon_layout) self.main_layout.addSpacing(10) self.main_layout.addWidget(content_label) self.main_layout.addStretch() close_timer = QTimer(self) close_timer.setSingleShot(True) close_timer.timeout.connect(self.close) close_timer.timeout.connect(self.toastClosed.emit) close_timer.setInterval( (self._duration or self.DEFAULT_CONFIG.get('duration', 2)) * 1000) anim_timer = QTimer(self) anim_timer.timeout.connect(self._fade_out) anim_timer.setInterval( (self._duration or self.DEFAULT_CONFIG.get('duration', 2)) * 1000 - 300) self._opacity_anim = QPropertyAnimation() self._opacity_anim.setTargetObject(self) self._opacity_anim.setDuration(300) self._opacity_anim.setEasingCurve(QEasingCurve.OutCubic) self._opacity_anim.setPropertyName('windowOpacity') self._opacity_anim.setStartValue(0.0) self._opacity_anim.setEndValue(0.9) close_timer.start() anim_timer.start() self._get_center_position(self._parent) self._fade_in() @classmethod def info(cls, text, parent, duration=None): inst = cls(text, duration=duration, toast_type=cls.ToastTypes.INFO, parent=parent) inst.show() return inst @classmethod def success(cls, text, parent, duration=None): inst = cls(text, duration=duration, toast_type=cls.ToastTypes.SUCCESS, parent=parent) inst.show() return inst @classmethod def warning(cls, text, parent, duration=None): inst = cls(text, duration=duration, toast_type=cls.ToastTypes.WARNING, parent=parent) inst.show() return inst @classmethod def error(cls, text, parent, duration=None): inst = cls(text, duration=duration, toast_type=cls.ToastTypes.ERROR, parent=parent) inst.show() return inst @classmethod def loading(cls, text, parent, duration=None): inst = cls(text, duration=duration, toast_type=cls.ToastTypes.LOADING, parent=parent) inst.show() return inst @classmethod def config(cls, duration): if duration is not None: cls.DEFAULT_CONFIG['duration'] = duration def _fade_out(self): self._opacity_anim.setDirection(QAbstractAnimation.Backward) self._opacity_anim.start() def _fade_in(self): self._opacity_anim.start() def _get_center_position(self, parent): parent_parent = parent.parent() dcc_win = dcc.get_main_window() if dcc_win: dcc_window = parent_parent == dcc_win or parent_parent.objectName( ) == dcc_win.objectName() else: dcc_window = None parent_geo = parent.geometry() pos = parent_geo.topLeft() if dcc_window else parent.mapToGlobal( parent_geo.topLeft()) offset = 0 for child in parent.children(): if isinstance(child, BaseToast) and child.isVisible(): offset = max(offset, child.y()) target_x = pos.x() + parent_geo.width() / 2 - self.width() / 2 target_y = pos.y() + parent_geo.height() / 2 - self.height() / 2 self.setProperty('pos', QPoint(target_x, target_y))
class PopupMessage(base.BaseWidget, object): """ Message that appears at the top of the window and shows feedback in response to user actions """ DEFAULT_CONFIG = {'duration': 2, 'top': 24} closed = Signal() def __init__(self, text, duration=None, theme_type=None, closable=False, parent=None): self._text = text self._duration = duration self._theme_type = theme_type self._closable = closable super(PopupMessage, self).__init__(parent=parent) self.setAttribute(Qt.WA_TranslucentBackground) close_timer = QTimer(self) close_timer.setSingleShot(True) close_timer.timeout.connect(self.close) close_timer.timeout.connect(self.closed) close_timer.setInterval( (duration or self.DEFAULT_CONFIG['duration']) * 1000) anim_timer = QTimer(self) anim_timer.timeout.connect(self._on_fade_out) anim_timer.setInterval((duration or self.DEFAULT_CONFIG['duration']) * 1000 - 300) close_timer.start() anim_timer.start() self._pos_anim = QPropertyAnimation(self) self._pos_anim.setTargetObject(self) self._pos_anim.setEasingCurve(QEasingCurve.OutCubic) self._pos_anim.setDuration(300) self._pos_anim.setPropertyName(b'pos') self._opacity_anim = QPropertyAnimation() self._opacity_anim.setTargetObject(self) self._opacity_anim.setEasingCurve(QEasingCurve.OutCubic) self._opacity_anim.setDuration(300) self._opacity_anim.setPropertyName(b'windowOpacity') self._opacity_anim.setStartValue(0.0) self._opacity_anim.setEndValue(1.0) self._set_proper_position(parent) self._fade_in() # ================================================================================================================= # OVERRIDES # ================================================================================================================= def get_main_layout(self): main_layout = layouts.HorizontalLayout() return main_layout def ui(self): super(PopupMessage, self).ui() current_theme = self.theme() self.setObjectName('message') self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog | Qt.WA_TranslucentBackground | Qt.WA_DeleteOnClose) # self.setAttribute(Qt.WA_TranslucentBackground) if self._theme_type == MessageTypes.LOADING: icon_label = loading.CircleLoading.tiny(parent=self) else: icon_label = avatar.Avatar.tiny() current_type = self._theme_type or MessageTypes.INFO if current_theme: icon_label.image = resources.pixmap( current_type, color=getattr(current_theme, '{}_color'.format(current_type))) main_frame = QFrame(self) main_frame_layout = layouts.HorizontalLayout(spacing=5, margins=(5, 5, 5, 5)) main_frame.setLayout(main_frame_layout) self.main_layout.addWidget(main_frame) self._content_label = label.BaseLabel(parent=self) self._content_label.setText(self._text) self._close_btn = buttons.BaseToolButton(parent=self).image( 'close', theme='window').icon_only().tiny() self._close_btn.setVisible(self._closable or False) main_frame_layout.addWidget(icon_label) main_frame_layout.addWidget(self._content_label) main_frame_layout.addStretch() main_frame_layout.addWidget(self._close_btn) def setup_signals(self): self._close_btn.clicked.connect(self.close) # ================================================================================================================= # BASE # ================================================================================================================= @classmethod def info(cls, text, parent, duration=None, closable=None): """ Shows an info message :param text: str :param parent: QWidget :param duration: int :param closable: bool :return: PopupMessage """ popup_message_inst = cls(text, theme_type=MessageTypes.INFO, duration=duration, closable=closable, parent=parent) popup_message_inst.show() return popup_message_inst @classmethod def success(cls, text, parent, duration=None, closable=None): """ Shows a success message :param text: str :param parent: QWidget :param duration: int :param closable: bool :return: PopupMessage """ popup_message_inst = cls(text, theme_type=MessageTypes.SUCCESS, duration=duration, closable=closable, parent=parent) popup_message_inst.show() return popup_message_inst @classmethod def warning(cls, text, parent, duration=None, closable=None): """ Shows a warning message :param text: str :param parent: QWidget :param duration: int :param closable: bool :return: PopupMessage """ popup_message_inst = cls(text, theme_type=MessageTypes.WARNING, duration=duration, closable=closable, parent=parent) popup_message_inst.show() return popup_message_inst @classmethod def error(cls, text, parent, duration=None, closable=None): """ Shows an error message :param text: str :param parent: QWidget :param duration: int :param closable: bool :return: PopupMessage """ popup_message_inst = cls(text, theme_type=MessageTypes.ERROR, duration=duration, closable=closable, parent=parent) popup_message_inst.show() return popup_message_inst @classmethod def loading(cls, text, parent, duration=None, closable=None): """ Shows a loading message :param text: str :param parent: QWidget :param duration: int :param closable: bool :return: PopupMessage """ popup_message_inst = cls(text, theme_type=MessageTypes.LOADING, duration=duration, closable=closable, parent=parent) popup_message_inst.show() return popup_message_inst @classmethod def config(cls, duration=None, top=None): """ Configures global PopupMesage duration and top setting :param duration: int (seconds) :param top: int (px) """ if duration is not None: cls.DEFAULT_CONFIG['duration'] = duration if top is not None: cls.DEFAULT_CONFIG['top'] = top # ================================================================================================================= # INTERNAL # ================================================================================================================= def _fade_out(self): self._pos_anim.setDirection(QAbstractAnimation.Backward) self._pos_anim.start() self._opacity_anim.setDirection(QAbstractAnimation.Backward) self._opacity_anim.start() def _fade_in(self): self._pos_anim.start() self._opacity_anim.start() def _set_proper_position(self, parent): parent_parent = parent.parent() dcc_win = dcc.get_main_window() if dcc_win: dcc_window = parent_parent == dcc_win or parent_parent.objectName( ) == dcc_win.objectName() else: dcc_window = None parent_geo = parent.geometry() pos = parent_geo.topLeft() if dcc_window else parent.mapToGlobal( parent_geo.topLeft()) # pos = parent_geo.topLeft() if parent.parent() is None else parent.mapToGlobal(parent_geo.topLeft()) offset = 0 for child in parent.children(): if isinstance(child, PopupMessage) and child.isVisible(): offset = max(offset, child.y()) base_pos = pos.y() + PopupMessage.DEFAULT_CONFIG.get('top') target_x = pos.x() + parent_geo.width() / 2 - 100 target_y = (offset + 50) if offset else base_pos self._pos_anim.setStartValue(QPoint(target_x, target_y - 40)) self._pos_anim.setEndValue(QPoint(target_x, target_y)) # ================================================================================================================= # CALLBACK # ================================================================================================================= def _on_fade_out(self): self._fade_out()
class SliderPanel(base.BaseWidget, object): """ Panel that slides in from the edge of the window """ closed = Signal() closeButtonClicked = Signal() def __init__(self, title, position=SliderPanelPositions.RIGHT, closable=True, parent=None): self._title = title self._position = position self._closable = closable self._is_first_close = True super(SliderPanel, self).__init__(parent) self.setObjectName('sliderPanel') self.setWindowFlags(Qt.Popup) self.setAttribute(Qt.WA_StyledBackground) self._close_timer = QTimer(self) self._close_timer.setInterval(300) self._close_timer.setSingleShot(True) self._close_timer.timeout.connect(self.close) self._close_timer.timeout.connect(self.closed.emit) self._pos_anim = QPropertyAnimation(self) self._pos_anim.setTargetObject(self) self._pos_anim.setEasingCurve(QEasingCurve.OutCubic) self._pos_anim.setDuration(300) self._pos_anim.setPropertyName(b'pos') self._opacity_anim = QPropertyAnimation() self._opacity_anim.setTargetObject(self) self._opacity_anim.setDuration(300) self._opacity_anim.setEasingCurve(QEasingCurve.OutCubic) self._opacity_anim.setPropertyName(b'windowOpacity') self._opacity_anim.setStartValue(0.0) self._opacity_anim.setEndValue(1.0) # ================================================================================================================= # PROPERTIES # ================================================================================================================= @property def position(self): """ Returns the placement of the panel in parent window :return: str """ return self._position @position.setter def position(self, value): """ Sets the position of the panel in parent window ('top', 'right', 'bottom' or 'left'). :param value: str """ self._position = value if value in [SliderPanelPositions.BOTTOM, SliderPanelPositions.TOP]: self.setFixedHeight(200) else: self.setFixedWidth(200) # ================================================================================================================= # OVERRIDES # ================================================================================================================= def ui(self): super(SliderPanel, self).ui() self._title_label = label.BaseLabel(parent=self).h4() self._title_label.setText(self._title) self._close_btn = buttons.BaseToolButton( parent=self).icon_only().image('close', theme='window').small() self._close_btn.setVisible(self._closable or False) title_layout = layouts.HorizontalLayout() title_layout.addWidget(self._title_label) title_layout.addStretch() title_layout.addWidget(self._close_btn) self._button_layout = layouts.HorizontalLayout() self._button_layout.addStretch() self._scroll_area = QScrollArea() self.main_layout.addLayout(title_layout) self.main_layout.addWidget(dividers.Divider()) self.main_layout.addWidget(self._scroll_area) self.main_layout.addWidget(dividers.Divider()) self.main_layout.addLayout(self._button_layout) def setup_signals(self): self._close_btn.clicked.connect(self.close) self._close_btn.clicked.connect(self.closeButtonClicked.emit) def show(self): self._update_position() self._fade_in() return super(SliderPanel, self).show() def closeEvent(self, event): if self._is_first_close: self._is_first_close = False self._close_timer.stop() self._fade_out() self.closed.emit() event.ignore() else: event.accept() # ================================================================================================================= # BASE # ================================================================================================================= def set_widget(self, widget): """ Sets the widget that will be contained inside the panel :param widget: QWidget """ self._scroll_area.setWidget(widget) def add_button(self, button): """ Adds a new button to the bottom part of the panel :param button: QPushButton """ self._button_layout.addWidget(button) def left(self): """ Sets the panel's placement to left :return: SliderPanel """ self.position = SliderPanelPositions.LEFT return self def right(self): """ Sets the panel's placement to right :return: SliderPanel """ self.position = SliderPanelPositions.RIGHT return self def top(self): """ Sets the panel's placement to top :return: SliderPanel """ self.position = SliderPanelPositions.TOP return self def bottom(self): """ Sets the panel's placement to bottom :return: SliderPanel """ self.position = SliderPanelPositions.BOTTOM return self # ================================================================================================================= # INTERNAL # ================================================================================================================= def _fade_in(self): """ Internal function that fades in the panel """ self._pos_anim.start() self._opacity_anim.start() def _fade_out(self): """ Internal function that fades out the panel """ self._pos_anim.setDirection(QAbstractAnimation.Backward) self._pos_anim.start() self._opacity_anim.setDirection(QAbstractAnimation.Backward) self._opacity_anim.start() def _update_position(self): """ Internal function that makes sure that panel is positioned in the proper place """ parent = self.parent() parent_parent = parent.parent() dcc_win = dcc.get_main_window() dcc_window = parent_parent == dcc_win if parent_parent and dcc_win: dcc_window = dcc_window or parent_parent.objectName( ) == dcc_win.objectName() parent_geo = parent.geometry() if self._position == SliderPanelPositions.LEFT: pos = parent_geo.topLeft() if dcc_window else parent.mapToGlobal( parent_geo.topLeft()) target_x = pos.x() target_y = pos.y() self.setFixedHeight(parent_geo.height()) self._pos_anim.setStartValue( QPoint(target_x - self.width(), target_y)) self._pos_anim.setEndValue(QPoint(target_x, target_y)) if self._position == SliderPanelPositions.RIGHT: pos = parent_geo.topRight() if dcc_window else parent.mapToGlobal( parent_geo.topRight()) self.setFixedHeight(parent_geo.height()) target_x = pos.x() - self.width() target_y = pos.y() self._pos_anim.setStartValue( QPoint(target_x + self.width(), target_y)) self._pos_anim.setEndValue(QPoint(target_x, target_y)) if self._position == SliderPanelPositions.TOP: pos = parent_geo.topLeft( ) if dcc_window or parent_parent is None else parent.mapToGlobal( parent_geo.topLeft()) self.setFixedWidth(parent_geo.width()) target_x = pos.x() target_y = pos.y() self._pos_anim.setStartValue( QPoint(target_x, target_y - self.height())) self._pos_anim.setEndValue(QPoint(target_x, target_y)) if self._position == SliderPanelPositions.BOTTOM: pos = parent_geo.bottomLeft( ) if dcc_window else parent.mapToGlobal(parent_geo.bottomLeft()) self.setFixedWidth(parent_geo.width()) target_x = pos.x() target_y = pos.y() - self.height() self._pos_anim.setStartValue( QPoint(target_x, target_y + self.height())) self._pos_anim.setEndValue(QPoint(target_x, target_y))