diff --git a/pype/tools/pyblish_pype/app.css b/pype/tools/pyblish_pype/app.css index 3a2c05c1f3..c51126e89f 100644 --- a/pype/tools/pyblish_pype/app.css +++ b/pype/tools/pyblish_pype/app.css @@ -459,7 +459,7 @@ QToolButton { color: #fff; } -#TerminalFilerBtn { +#TerminalFilterWidget QPushButton { /* font: %(font_size_pt)spt; */ font-family: "FontAwesome"; text-align: center; @@ -468,29 +468,58 @@ QToolButton { border-color: #777777; border-style: none; padding: 0px; - border-radius: 3px; + border-radius: 8px; +} +#TerminalFilterWidget QPushButton:hover { + background: #5f5f5f; + border-style: none; +} +#TerminalFilterWidget QPushButton:pressed { + background: #606060; + border-style: none; +} +#TerminalFilterWidget QPushButton:pressed:hover { + background: #626262; + border-style: none; } #TerminalFilerBtn[type="info"]:checked {color: rgb(255, 255, 255);} +#TerminalFilerBtn[type="info"]:hover:pressed {color: rgba(255, 255, 255, 163);} #TerminalFilerBtn[type="info"] {color: rgba(255, 255, 255, 63);} #TerminalFilerBtn[type="error"]:checked {color: rgb(255, 74, 74);} +#TerminalFilerBtn[type="error"]:hover:pressed {color: rgba(255, 74, 74, 163);} #TerminalFilerBtn[type="error"] {color: rgba(255, 74, 74, 63);} #TerminalFilerBtn[type="log_debug"]:checked {color: rgb(255, 102, 232);} #TerminalFilerBtn[type="log_debug"] {color: rgba(255, 102, 232, 63);} +#TerminalFilerBtn[type="log_debug"]:hover:pressed { + color: rgba(255, 102, 232, 163); +} #TerminalFilerBtn[type="log_info"]:checked {color: rgb(102, 171, 255);} #TerminalFilerBtn[type="log_info"] {color: rgba(102, 171, 255, 63);} +#TerminalFilerBtn[type="log_info"]:hover:pressed { + color: rgba(102, 171, 255, 163); +} #TerminalFilerBtn[type="log_warning"]:checked {color: rgb(255, 186, 102);} #TerminalFilerBtn[type="log_warning"] {color: rgba(255, 186, 102, 63);} +#TerminalFilerBtn[type="log_warning"]:hover:pressed { + color: rgba(255, 186, 102, 163); +} #TerminalFilerBtn[type="log_error"]:checked {color: rgb(255, 77, 88);} #TerminalFilerBtn[type="log_error"] {color: rgba(255, 77, 88, 63);} +#TerminalFilerBtn[type="log_error"]:hover:pressed { + color: rgba(255, 77, 88, 163); +} #TerminalFilerBtn[type="log_critical"]:checked {color: rgb(255, 79, 117);} #TerminalFilerBtn[type="log_critical"] {color: rgba(255, 79, 117, 63);} +#TerminalFilerBtn[type="log_critical"]:hover:pressed { + color: rgba(255, 79, 117, 163); +} #SuspendLogsBtn { background: #444; diff --git a/pype/tools/pyblish_pype/app.py b/pype/tools/pyblish_pype/app.py index 0f662d5b3e..9879c63030 100644 --- a/pype/tools/pyblish_pype/app.py +++ b/pype/tools/pyblish_pype/app.py @@ -92,7 +92,6 @@ def show(parent=None): self._window.show() self._window.activateWindow() - self._window.resize(*settings.WindowSize) self._window.setWindowTitle(settings.WindowTitle) font = QtGui.QFont("Open Sans", 8, QtGui.QFont.Normal) @@ -100,5 +99,6 @@ def show(parent=None): self._window.setStyleSheet(css) self._window.reset() + self._window.resize(*settings.WindowSize) return self._window diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 477303eae8..248c1fbbf9 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -83,6 +83,7 @@ class OverviewView(QtWidgets.QTreeView): self.setHeaderHidden(True) self.setRootIsDecorated(False) self.setIndentation(0) + self.setAnimated(True) def event(self, event): if not event.type() == QtCore.QEvent.KeyPress: @@ -159,6 +160,8 @@ class InstanceView(OverviewView): def __init__(self, parent=None): super(InstanceView, self).__init__(parent) self.viewport().setMouseTracking(True) + self._pressed_group_index = None + self._pressed_expander = None def mouseMoveEvent(self, event): index = self.indexAt(event.pos()) @@ -176,6 +179,8 @@ class InstanceView(OverviewView): self.collapse(index) def group_toggle(self, index): + if not index.isValid(): + return model = index.model() chilren_indexes_checked = [] @@ -201,25 +206,90 @@ class InstanceView(OverviewView): model.setData(index, new_state, QtCore.Qt.CheckStateRole) self.toggled.emit(index, new_state) - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: + def _mouse_press(self, event): + if event.button() != QtCore.Qt.LeftButton: + return + + self._pressed_group_index = None + self._pressed_expander = None + + pos_index = self.indexAt(event.pos()) + if not pos_index.isValid(): + return + + if pos_index.data(Roles.TypeRole) != model.InstanceType: + self._pressed_group_index = pos_index + if event.pos().x() < 20: + self._pressed_expander = True + else: + self._pressed_expander = False + + elif event.pos().x() < 20: indexes = self.selectionModel().selectedIndexes() - if len(indexes) == 1: - index = indexes[0] - pos_index = self.indexAt(event.pos()) - if index == pos_index: - # If instance or Plugin - if index.data(Roles.TypeRole) == model.InstanceType: - if event.pos().x() < 20: - self.toggled.emit(index, None) - elif event.pos().x() > self.width() - 20: - self.show_perspective.emit(index) - else: - if event.pos().x() < EXPANDER_WIDTH: - self.item_expand(index) - else: - self.group_toggle(index) - self.item_expand(index, True) + any_checked = False + if len(indexes) <= 1: + return + + if pos_index in indexes: + for index in indexes: + if index.data(QtCore.Qt.CheckStateRole): + any_checked = True + break + + for index in indexes: + self.toggled.emit(index, not any_checked) + return True + self.toggled.emit(pos_index, not any_checked) + + def mousePressEvent(self, event): + if self._mouse_press(event): + return + return super(InstanceView, self).mousePressEvent(event) + + def _mouse_release(self, event, pressed_expander, pressed_index): + if event.button() != QtCore.Qt.LeftButton: + return + + pos_index = self.indexAt(event.pos()) + if not pos_index.isValid(): + return + + if pos_index.data(Roles.TypeRole) == model.InstanceType: + indexes = self.selectionModel().selectedIndexes() + if len(indexes) == 1 and indexes[0] == pos_index: + if event.pos().x() < 20: + self.toggled.emit(indexes[0], None) + elif event.pos().x() > self.width() - 20: + self.show_perspective.emit(indexes[0]) + return True + return + + if pressed_index != pos_index: + return + + if self.state() == QtWidgets.QTreeView.State.DragSelectingState: + indexes = self.selectionModel().selectedIndexes() + if len(indexes) != 1 or indexes[0] != pos_index: + return + + if event.pos().x() < EXPANDER_WIDTH: + if pressed_expander is True: + self.item_expand(pos_index) + return True + else: + if pressed_expander is False: + self.group_toggle(pos_index) + self.item_expand(pos_index, True) + return True + + def mouseReleaseEvent(self, event): + pressed_index = self._pressed_group_index + pressed_expander = self._pressed_expander is True + self._pressed_group_index = None + self._pressed_expander = None + result = self._mouse_release(event, pressed_expander, pressed_index) + if result: + return return super(InstanceView, self).mouseReleaseEvent(event) diff --git a/pype/tools/pyblish_pype/widgets.py b/pype/tools/pyblish_pype/widgets.py index 880d4755ad..4da759899e 100644 --- a/pype/tools/pyblish_pype/widgets.py +++ b/pype/tools/pyblish_pype/widgets.py @@ -6,7 +6,7 @@ from .constants import PluginStates, InstanceStates, Roles class EllidableLabel(QtWidgets.QLabel): def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) + super(EllidableLabel, self).__init__(*args, **kwargs) self.setObjectName("EllidableLabel") def paintEvent(self, event): @@ -21,7 +21,7 @@ class EllidableLabel(QtWidgets.QLabel): class PerspectiveLabel(QtWidgets.QTextEdit): def __init__(self, parent=None): - super(self.__class__, self).__init__(parent) + super(PerspectiveLabel, self).__init__(parent) self.setObjectName("PerspectiveLabel") size_policy = self.sizePolicy() @@ -50,7 +50,7 @@ class PerspectiveLabel(QtWidgets.QTextEdit): return margins.top() + document.size().height() + margins.bottom() def sizeHint(self): - width = super(self.__class__, self).sizeHint().width() + width = super(PerspectiveLabel, self).sizeHint().width() return QtCore.QSize(width, self.heightForWidth(width)) @@ -328,7 +328,7 @@ class PerspectiveWidget(QtWidgets.QWidget): self.records.toggle_content(len_records > 0) def toggle_me(self): - self.parent_widget.toggle_perspective_widget() + self.parent_widget.parent().toggle_perspective_widget() class ClickableWidget(QtWidgets.QLabel): @@ -407,7 +407,7 @@ class ExpandableWidget(QtWidgets.QWidget): self.content_widget.setVisible(checked) def resizeEvent(self, event): - super(self.__class__, self).resizeEvent(event) + super(ExpandableWidget, self).resizeEvent(event) self.content.updateGeometry() def set_content(self, in_widget): @@ -481,7 +481,7 @@ class CommentBox(QtWidgets.QLineEdit): class TerminalDetail(QtWidgets.QTextEdit): def __init__(self, text, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) + super(TerminalDetail, self).__init__(*args, **kwargs) self.setReadOnly(True) self.setHtml(text) @@ -504,7 +504,7 @@ class FilterButton(QtWidgets.QPushButton): def __init__(self, name, *args, **kwargs): self.filter_name = name - super(self.__class__, self).__init__(*args, **kwargs) + super(FilterButton, self).__init__(*args, **kwargs) self.toggled.connect(self.on_toggle) @@ -522,8 +522,8 @@ class FilterButton(QtWidgets.QPushButton): class TerminalFilterWidget(QtWidgets.QWidget): # timer.timeout.connect(lambda: self._update(self.parent_widget)) def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - + super(TerminalFilterWidget, self).__init__(*args, **kwargs) + self.setObjectName("TerminalFilterWidget") self.filter_changed = QtCore.Signal() info_icon = awesome.tags["info"] @@ -531,16 +531,16 @@ class TerminalFilterWidget(QtWidgets.QWidget): error_icon = awesome.tags["exclamation-triangle"] filter_buttons = ( - FilterButton("info", info_icon), - FilterButton("log_debug", log_icon), - FilterButton("log_info", log_icon), - FilterButton("log_warning", log_icon), - FilterButton("log_error", log_icon), - FilterButton("log_critical", log_icon), - FilterButton("error", error_icon) + FilterButton("info", info_icon, self), + FilterButton("log_debug", log_icon, self), + FilterButton("log_info", log_icon, self), + FilterButton("log_warning", log_icon, self), + FilterButton("log_error", log_icon, self), + FilterButton("log_critical", log_icon, self), + FilterButton("error", error_icon, self) ) - layout = QtWidgets.QHBoxLayout() + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # Add spacers layout.addWidget(QtWidgets.QWidget(), 1) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index b7aada5ff7..ab59f9f6a1 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -39,6 +39,7 @@ Todo: the first time to understand how to actually to it! """ +import sys from functools import partial from . import delegate, model, settings, util, view, widgets @@ -48,6 +49,10 @@ from Qt import QtCore, QtGui, QtWidgets from .constants import ( PluginStates, PluginActionStates, InstanceStates, GroupStates, Roles ) +if sys.version_info[0] == 3: + from queue import Queue +else: + from Queue import Queue class Window(QtWidgets.QDialog): @@ -266,6 +271,7 @@ class Window(QtWidgets.QDialog): layout.addWidget(footer_button_play, 0) footer_layout = QtWidgets.QVBoxLayout(footer_widget) + footer_layout.addWidget(terminal_filters_widget) footer_layout.addWidget(comment_intent_widget) footer_layout.addLayout(layout) @@ -280,16 +286,21 @@ class Window(QtWidgets.QDialog): ) closing_placeholder.hide() - perspective_widget = widgets.PerspectiveWidget(self) + perspective_widget = widgets.PerspectiveWidget(main_widget) perspective_widget.hide() + pages_widget = QtWidgets.QWidget(main_widget) + layout = QtWidgets.QVBoxLayout(pages_widget) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(header_widget, 0) + layout.addWidget(body_widget, 1) + # Main layout layout = QtWidgets.QVBoxLayout(main_widget) - layout.addWidget(header_widget, 0) - layout.addWidget(body_widget, 3) + layout.addWidget(pages_widget, 3) layout.addWidget(perspective_widget, 3) layout.addWidget(closing_placeholder, 1) - layout.addWidget(terminal_filters_widget, 0) layout.addWidget(footer_widget, 0) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) @@ -381,6 +392,7 @@ class Window(QtWidgets.QDialog): # Enable CSS on plain QWidget objects for _widget in ( + pages_widget, header_widget, body_widget, artist_page, @@ -456,6 +468,7 @@ class Window(QtWidgets.QDialog): self.main_widget = main_widget + self.pages_widget = pages_widget self.header_widget = header_widget self.body_widget = body_widget @@ -497,13 +510,20 @@ class Window(QtWidgets.QDialog): "overview": header_tab_overview, "terminal": header_tab_terminal } - self.pages = { - "artist": artist_page, - "overview": overview_page, - "terminal": terminal_page - } + self.pages = ( + ("artist", artist_page), + ("overview", overview_page), + ("terminal", terminal_page) + ) current_page = settings.InitialTab or "artist" + self.comment_main_widget.setVisible( + not current_page == "terminal" + ) + self.terminal_filters_widget.setVisible( + current_page == "terminal" + ) + self.state = { "is_closing": False, "current_page": current_page @@ -547,11 +567,9 @@ class Window(QtWidgets.QDialog): show = True self.perspective_widget.set_context(index) - self.body_widget.setVisible(not show) - self.header_widget.setVisible(not show) - + self.pages_widget.setVisible(not show) self.perspective_widget.setVisible(show) - self.terminal_filters_widget.setVisible(show) + self.footer_items_visibility() def change_toggleability(self, enable_value): for plugin_item in self.plugin_model.plugin_items.values(): @@ -616,19 +634,232 @@ class Window(QtWidgets.QDialog): self.update_compatibility() def on_tab_changed(self, target): - self.comment_main_widget.setVisible(not target == "terminal") - self.terminal_filters_widget.setVisible(target == "terminal") - - for name, page in self.pages.items(): - if name != target: - page.hide() - - self.pages[target].show() + previous_page = None + target_page = None + direction = None + for name, page in self.pages: + if name == target: + target_page = page + if direction is None: + direction = -1 + elif name == self.state["current_page"]: + previous_page = page + if direction is None: + direction = 1 + else: + page.setVisible(False) self.state["current_page"] = target + self.slide_page(previous_page, target_page, direction) + + def slide_page(self, previous_page, target_page, direction): + if previous_page is None: + for name, page in self.pages: + for _name, _page in self.pages: + if name != _name: + _page.hide() + page.show() + page.hide() + + if ( + previous_page == target_page + or previous_page is None + ): + if not target_page.isVisible(): + target_page.show() + return + + width = previous_page.frameGeometry().width() + offset = QtCore.QPoint(direction * width, 0) + + previous_rect = ( + previous_page.frameGeometry().x(), + previous_page.frameGeometry().y(), + width, + previous_page.frameGeometry().height() + ) + curr_pos = previous_page.pos() + + previous_page.hide() + target_page.show() + target_page.update() + target_rect = ( + target_page.frameGeometry().x(), + target_page.frameGeometry().y(), + target_page.frameGeometry().width(), + target_page.frameGeometry().height() + ) + previous_page.show() + + target_page.raise_() + previous_page.setGeometry(*previous_rect) + target_page.setGeometry(*target_rect) + + target_page.move(curr_pos + offset) + + duration = 250 + + anim_old = QtCore.QPropertyAnimation( + previous_page, b"pos", self + ) + anim_old.setDuration(duration) + anim_old.setStartValue(curr_pos) + anim_old.setEndValue(curr_pos - offset) + anim_old.setEasingCurve(QtCore.QEasingCurve.OutQuad) + + anim_new = QtCore.QPropertyAnimation( + target_page, b"pos", self + ) + anim_new.setDuration(duration) + anim_new.setStartValue(curr_pos + offset) + anim_new.setEndValue(curr_pos) + anim_new.setEasingCurve(QtCore.QEasingCurve.OutQuad) + + anim_group = QtCore.QParallelAnimationGroup(self) + anim_group.addAnimation(anim_old) + anim_group.addAnimation(anim_new) + + def slide_finished(): + previous_page.hide() + self.footer_items_visibility() + + anim_group.finished.connect(slide_finished) + anim_group.start() + + def footer_items_visibility( + self, + comment_visible=None, + terminal_filters_visibile=None + ): + target = self.state["current_page"] + comment_visibility = ( + not self.perspective_widget.isVisible() + and not target == "terminal" + and self.comment_box.isEnabled() + ) + terminal_filters_visibility = ( + target == "terminal" + or self.perspective_widget.isVisible() + ) + + if comment_visible is not None and comment_visibility: + comment_visibility = comment_visible + + if ( + terminal_filters_visibile is not None + and terminal_filters_visibility + ): + terminal_filters_visibility = terminal_filters_visibile + + duration = 150 + + hiding_widgets = [] + showing_widgets = [] + if (comment_visibility != ( + self.comment_main_widget.isVisible() + )): + if self.comment_main_widget.isVisible(): + hiding_widgets.append(self.comment_main_widget) + else: + showing_widgets.append(self.comment_main_widget) + + if (terminal_filters_visibility != ( + self.terminal_filters_widget.isVisible() + )): + if self.terminal_filters_widget.isVisible(): + hiding_widgets.append(self.terminal_filters_widget) + else: + showing_widgets.append(self.terminal_filters_widget) + + if not hiding_widgets and not showing_widgets: + return + + hiding_widgets_queue = Queue() + showing_widgets_queue = Queue() + widgets_by_pos_y = {} + for widget in hiding_widgets: + key = widget.mapToGlobal(widget.rect().topLeft()).x() + widgets_by_pos_y[key] = widget + + for key in sorted(widgets_by_pos_y.keys()): + widget = widgets_by_pos_y[key] + hiding_widgets_queue.put((widget, )) + + for widget in hiding_widgets: + widget.hide() + + for widget in showing_widgets: + widget.show() + + self.footer_widget.updateGeometry() + widgets_by_pos_y = {} + for widget in showing_widgets: + key = widget.mapToGlobal(widget.rect().topLeft()).x() + widgets_by_pos_y[key] = widget + + for key in reversed(sorted(widgets_by_pos_y.keys())): + widget = widgets_by_pos_y[key] + showing_widgets_queue.put(widget) + + for widget in showing_widgets: + widget.hide() + + for widget in hiding_widgets: + widget.show() + + def process_showing(): + if showing_widgets_queue.empty(): + return + + widget = showing_widgets_queue.get() + widget.show() + + widget_rect = widget.frameGeometry() + second_rect = QtCore.QRect(widget_rect) + second_rect.setTopLeft(second_rect.bottomLeft()) + + animation = QtCore.QPropertyAnimation( + widget, b"geometry", self + ) + animation.setDuration(duration) + animation.setStartValue(second_rect) + animation.setEndValue(widget_rect) + animation.setEasingCurve(QtCore.QEasingCurve.OutQuad) + + animation.finished.connect(process_showing) + animation.start() + + def process_hiding(): + if hiding_widgets_queue.empty(): + return process_showing() + + item = hiding_widgets_queue.get() + if isinstance(item, tuple): + widget = item[0] + hiding_widgets_queue.put(widget) + widget_rect = widget.frameGeometry() + second_rect = QtCore.QRect(widget_rect) + second_rect.setTopLeft(second_rect.bottomLeft()) + + anim = QtCore.QPropertyAnimation( + widget, b"geometry", self + ) + anim.setDuration(duration) + anim.setStartValue(widget_rect) + anim.setEndValue(second_rect) + anim.setEasingCurve(QtCore.QEasingCurve.OutQuad) + + anim.finished.connect(process_hiding) + anim.start() + else: + item.hide() + return process_hiding() + + process_hiding() def on_validate_clicked(self): self.comment_box.setEnabled(False) + self.footer_items_visibility() self.intent_box.setEnabled(False) self._add_intent_to_context() @@ -637,6 +868,7 @@ class Window(QtWidgets.QDialog): def on_play_clicked(self): self.comment_box.setEnabled(False) + self.footer_items_visibility() self.intent_box.setEnabled(False) self._add_intent_to_context() @@ -661,7 +893,7 @@ class Window(QtWidgets.QDialog): def apply_log_suspend_value(self, value): self._suspend_logs = value if self.state["current_page"] == "terminal": - self.on_tab_changed("overview") + self.tabs["overview"].setChecked(True) self.tabs["terminal"].setVisible(not self._suspend_logs) @@ -758,6 +990,7 @@ class Window(QtWidgets.QDialog): comment = self.controller.context.data.get("comment") self.comment_box.setText(comment or None) self.comment_box.setEnabled(True) + self.footer_items_visibility() self.intent_box.setEnabled(True)