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 _animate_expand(self, value): size_anim = QPropertyAnimation(self, 'geometry') geometry = self.geometry() width = geometry.width() x, y, _, _ = geometry.getCoords() size_start = QRect(x, y, width, int(not (value)) * 150) size_end = QRect(x, y, width, value * 150) size_anim.setStartValue(size_start) size_anim.setEndValue(size_end) size_anim.setDuration(300) size_anim_curve = QEasingCurve() if value: size_anim_curve.setType(QEasingCurve.InQuad) else: size_anim_curve.setType(QEasingCurve.OutQuad) size_anim.setEasingCurve(size_anim_curve) # =================================================== Animation Sequence self._animation = QSequentialAnimationGroup() self._animation.addAnimation(size_anim) size_anim.valueChanged.connect(self._force_resize) if not value: self._animation.finished.connect(self.delete_widget) self._animation.start(QAbstractAnimation.DeleteWhenStopped)
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 slide_in_index(self, next, force=False): """ Slides to the given widget index :param next: int, index of the widget to slide """ now = self.currentIndex() if (self._active_state or next == now) and not force: return self._active_state = True width, height = self.frameRect().width(), self.frameRect().height() next %= self.count() if next > now: if self._vertical: offset_x, offset_y = 0, height else: offset_x, offset_y = width, 0 else: if self._vertical: offset_x, offset_y = 0, -height else: offset_x, offset_y = -width, 0 self.widget(next).setGeometry(0, 0, width, height) pnow, pnext = self.widget(now).pos(), self.widget(next).pos() self._point_now = pnow self.widget(next).move(pnext.x() + offset_x, pnext.y() + offset_y) self.widget(next).show() self.widget(next).raise_() self._current_widget = self.widget(next) anim_now = QPropertyAnimation(self.widget(now), b'pos') anim_now.setDuration(self._speed) anim_now.setStartValue(pnow) anim_now.setEndValue(QPoint(pnow.x() - offset_x, pnow.y() - offset_y)) anim_now.setEasingCurve(self._animation_type) anim_next = QPropertyAnimation(self.widget(next), b'pos') anim_next.setDuration(self._speed) anim_next.setStartValue( QPoint(offset_x + pnext.x(), offset_y + pnext.y())) anim_next.setEndValue(pnext) anim_next.setEasingCurve(self._animation_type) self._anim_group = QParallelAnimationGroup() self._anim_group.addAnimation(anim_now) self._anim_group.addAnimation(anim_next) self._anim_group.finished.connect(self._animation_done_slot) self._anim_group.start() self._next = next self._now = now
def animate_expand(self, value): size_animation = QPropertyAnimation(self, b'geometry') geometry = self.geometry() width = geometry.width() x, y, _, _ = geometry.getCoords() size_start = QRect(x, y, width, int(not value) * self.INTERP_HEIGHT) size_end = QRect(x, y, width, value * 150) size_animation.setStartValue(size_start) size_animation.setEndValue(size_end) size_animation.setDuration(200) size_anim_curve = QEasingCurve() size_anim_curve.setType( QEasingCurve.InQuad) if value else size_anim_curve.setType( QEasingCurve.OutQuad) size_animation.setEasingCurve(size_anim_curve) opacity_animation = QPropertyAnimation(self._main_widget_proxy, b'opacity') opacity_animation.setStartValue(not (value)) opacity_animation.setEndValue(value) opacity_animation.setDuration(100) opacity_anim_curve = QEasingCurve() opacity_anim_curve.setType( QEasingCurve.InQuad) if value else opacity_anim_curve.setType( QEasingCurve.OutQuad) opacity_animation.setEasingCurve(opacity_anim_curve) # We must store the animation objects as a member variables. Otherwise the animation object could be deleted # once the function is completed. In that case, the animation will not work. self._animation = QSequentialAnimationGroup() if value: self._main_widget_proxy.setOpacity(0) self._animation.addAnimation(size_animation) self._animation.addAnimation(opacity_animation) else: self._main_widget_proxy.setOpacity(1) self._animation.addAnimation(opacity_animation) self._animation.addAnimation(size_animation) # When animating geometry property, the parent layout is not updated automatically. # We force the resize of the layout by calling a signal each time the size animation value changes. size_animation.valueChanged.connect(self._on_force_resize) self._animation.finished.connect(self._animation.clear) if not value: self._animation.finished.connect(self._on_delete_widget) self._animation.start(QAbstractAnimation.DeleteWhenStopped)
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))
class JoyPad(QWidget, object): xChanged = Signal(float) yChanged = Signal(float) def __init__(self, parent=None): super(JoyPad, self).__init__(parent=parent) self._x = 0 self._y = 0 self._bounds = QRectF() self._knop_bounds = QRectF() self._last_pos = QPoint() self._knop_pressed = False self._return_animation = QParallelAnimationGroup(self) self._x_anim = QPropertyAnimation(self, 'x') self._y_anim = QPropertyAnimation(self, 'y') self._alignment = Qt.AlignTop | Qt.AlignLeft self._x_anim.setEndValue(0.0) self._x_anim.setDuration(400) self._x_anim.setEasingCurve(QEasingCurve.OutSine) self._y_anim.setEndValue(0.0) self._y_anim.setDuration(400) self._y_anim.setEasingCurve(QEasingCurve.OutSine) self._return_animation.addAnimation(self._x_anim) self._return_animation.addAnimation(self._y_anim) # region Static Functions @staticmethod def constraint(value, min_value, max_value): return min_value if value < min_value else max_value if value > max_value else value # endregion # region Properties def get_x(self): return self._x def set_x(self, x): self._x = self.constraint(x, -1.0, 1.0) radius = (self._bounds.width() - self._knop_bounds.width()) * 0.5 self._knop_bounds.moveCenter( QPointF(self._bounds.center().x() + self._x * radius, self._knop_bounds.center().y())) self.update() self.xChanged.emit(self._x) def get_y(self): return self._y def set_y(self, y): self._y = self.constraint(y, -1.0, 1.0) radius = (self._bounds.width() - self._knop_bounds.width()) * 0.5 self._knop_bounds.moveCenter( QPointF(self._knop_bounds.center().x(), self._bounds.center().y() - self._y * radius)) self.update() self.yChanged.emit(self._x) def get_alignment(self): return self._alignment def set_alignment(self, alignment): self._alignment = alignment x = property(get_x, set_x) y = property(get_y, set_y) alignment = property(get_alignment, set_alignment) # endregion # region Override Functions def resizeEvent(self, event): a = min(self.width(), self.height()) top_left = QPointF() if self._alignment & Qt.AlignTop: top_left.setY(0) elif self._alignment & Qt.AlignVCenter: top_left.setY((self.height() - a) * 0.5) elif self._alignment & Qt.AlignBottom: top_left.setY(self.height() - a) if self._alignment & Qt.AlignLeft: top_left.setX(0) elif self._alignment & Qt.AlignHCenter: top_left.setX((self.width() - a) * 0.5) elif self._alignment & Qt.AlignRight: top_left.setX(self.width() - a) self._bounds = QRectF(top_left, QSize(a, a)) self._knop_bounds.setWidth(a * 0.3) self._knop_bounds.setHeight(a * 0.3) radius = (self._bounds.width() - self._knop_bounds.height()) * 0.5 self._knop_bounds.moveCenter( QPointF(self._bounds.center().x() + self._x * radius, self._bounds.center().y() - self._y * radius)) def mousePressEvent(self, event): if self._knop_bounds.contains(event.pos()): # self._return_animation.stop() self._last_pos = event.pos() self._knop_pressed = True def mouseReleaseEvent(self, event): self._knop_pressed = False self.x = 0.0 self.y = 0.0 # self._return_animation.start() def mouseMoveEvent(self, event): if not self._knop_pressed: return delta_pos = QPointF(event.pos() - self._last_pos) delta_pos += 0.5 * (QPointF(event.pos()) - self._knop_bounds.center()) from_center_to_knop = self._knop_bounds.center( ) + delta_pos - self._bounds.center() radius = (self._bounds.width() - self._knop_bounds.width()) * 0.5 from_center_to_knop.setX( self.constraint(from_center_to_knop.x(), -radius, radius)) from_center_to_knop.setY( self.constraint(from_center_to_knop.y(), -radius, radius)) self._knop_bounds.moveCenter(from_center_to_knop + self._bounds.center()) self._last_pos = event.pos() self.update() if radius == 0: return x = (self._knop_bounds.center().x() - self._bounds.center().x()) / radius y = (-self._knop_bounds.center().y() + self._bounds.center().y()) / radius if self._x != x: self._x = x self.xChanged.emit(self._x) if self._y != y: self._y = y self.yChanged.emit(self._y) def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.HighQualityAntialiasing) gradient = QRadialGradient(self._bounds.center(), self._bounds.width() * 0.5, self._bounds.center()) gradient.setFocalRadius(self._bounds.width() * 0.3) gradient.setCenterRadius(self._bounds.width() * 0.7) gradient.setColorAt(0, Qt.white) gradient.setColorAt(1, Qt.lightGray) painter.setPen(QPen(QBrush(Qt.gray), self._bounds.width() * 0.005)) painter.setBrush(QBrush(gradient)) painter.drawEllipse(self._bounds) painter.setPen(QPen(QBrush(Qt.gray), self._bounds.width() * 0.005)) painter.drawLine( QPointF(self._bounds.left(), self._bounds.center().y()), QPointF(self._bounds.center().x() - self._bounds.width() * 0.35, self._bounds.center().y())) painter.drawLine( QPointF(self._bounds.center().x() + self._bounds.width() * 0.35, self._bounds.center().y()), QPointF(self._bounds.right(), self._bounds.center().y())) painter.drawLine( QPointF(self._bounds.center().x(), self._bounds.top()), QPointF(self._bounds.center().x(), self._bounds.center().y() - self._bounds.width() * 0.35)) painter.drawLine( QPointF(self._bounds.center().x(), self._bounds.center().y() + self._bounds.width() * 0.35), QPointF(self._bounds.center().x(), self._bounds.bottom())) if not self.isEnabled(): return gradient = QRadialGradient(self._knop_bounds.center(), self._knop_bounds.width() * 0.5, self._knop_bounds.center()) gradient.setFocalRadius(self._knop_bounds.width() * 0.2) gradient.setCenterRadius(self._knop_bounds.width() * 0.5) gradient.setColorAt(0, Qt.gray) gradient.setColorAt(1, Qt.darkGray) painter.setPen(QPen(QBrush(Qt.darkGray), self._bounds.width() * 0.005)) painter.setBrush(QBrush(gradient)) painter.drawEllipse(self._knop_bounds) # endregion # region Public Functions def add_x_animation(self): # Abort if the animation is already added if self._x_anim.parent() == self._return_animation: return self._return_animation.addAnimation(self._x_anim) def remove_x_animation(self): # Abort if the animation is already removed if self._x_anim.parent() != self._return_animation: return self._return_animation.removeAnimation(self._x_anim) # Take ownership of the animation (parent is 0 after removeAnimation()) self._x_anim.setParent(self) def add_y_animation(self): # Abort if the animation is already added if self._y_anim.parent() == self._return_animation: return self._return_animation.addAnimation(self._y_anim) def remove_y_animation(self): # Abort if the animation is already removed if self._y_anim.parent() != self._return_animation: return self._return_animation.removeAnimation(self._y_anim) # Take ownership of the animation (parent is 0 after removeAnimation()) self._y_anim.setParent(self)