From a27eab6049c3cb910e53eda2eb0de48249ffb653 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 21:41:33 +0200 Subject: [PATCH 01/31] init commit --- pype/tools/launcher/__init__.py | 10 + pype/tools/launcher/__main__.py | 5 + pype/tools/launcher/actions.py | 95 ++++ pype/tools/launcher/app.py | 717 ++++++++++++++++++++++++++++++ pype/tools/launcher/flickcharm.py | 305 +++++++++++++ pype/tools/launcher/lib.py | 67 +++ pype/tools/launcher/models.py | 292 ++++++++++++ pype/tools/launcher/widgets.py | 419 +++++++++++++++++ 8 files changed, 1910 insertions(+) create mode 100644 pype/tools/launcher/__init__.py create mode 100644 pype/tools/launcher/__main__.py create mode 100644 pype/tools/launcher/actions.py create mode 100644 pype/tools/launcher/app.py create mode 100644 pype/tools/launcher/flickcharm.py create mode 100644 pype/tools/launcher/lib.py create mode 100644 pype/tools/launcher/models.py create mode 100644 pype/tools/launcher/widgets.py diff --git a/pype/tools/launcher/__init__.py b/pype/tools/launcher/__init__.py new file mode 100644 index 0000000000..3b88ebe984 --- /dev/null +++ b/pype/tools/launcher/__init__.py @@ -0,0 +1,10 @@ + +from .app import ( + show, + cli +) + +__all__ = [ + "show", + "cli", +] diff --git a/pype/tools/launcher/__main__.py b/pype/tools/launcher/__main__.py new file mode 100644 index 0000000000..50642c46cd --- /dev/null +++ b/pype/tools/launcher/__main__.py @@ -0,0 +1,5 @@ +from app import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py new file mode 100644 index 0000000000..2a2e2ab0f0 --- /dev/null +++ b/pype/tools/launcher/actions.py @@ -0,0 +1,95 @@ +import os +import importlib + +from avalon import api, lib + + +class ProjectManagerAction(api.Action): + name = "projectmanager" + label = "Project Manager" + icon = "gear" + group = "Test" + order = 999 # at the end + + def is_compatible(self, session): + return "AVALON_PROJECT" in session + + def process(self, session, **kwargs): + return lib.launch(executable="python", + args=["-u", "-m", "avalon.tools.projectmanager", + session['AVALON_PROJECT']]) + + +class LoaderAction(api.Action): + name = "loader" + label = "Loader" + icon = "cloud-download" + order = 998 # at the end + group = "Test" + + def is_compatible(self, session): + return "AVALON_PROJECT" in session + + def process(self, session, **kwargs): + return lib.launch(executable="python", + args=["-u", "-m", "avalon.tools.cbloader", + session['AVALON_PROJECT']]) + + +class LoaderLibrary(api.Action): + name = "loader_os" + label = "Library Loader" + icon = "book" + order = 997 # at the end + + def is_compatible(self, session): + return True + + def process(self, session, **kwargs): + return lib.launch(executable="python", + args=["-u", "-m", "avalon.tools.libraryloader"]) + + +def register_default_actions(): + """Register default actions for Launcher""" + api.register_plugin(api.Action, ProjectManagerAction) + api.register_plugin(api.Action, LoaderAction) + api.register_plugin(api.Action, LoaderLibrary) + + +def register_config_actions(): + """Register actions from the configuration for Launcher""" + + module_name = os.environ["AVALON_CONFIG"] + config = importlib.import_module(module_name) + if not hasattr(config, "register_launcher_actions"): + print("Current configuration `%s` has no 'register_launcher_actions'" + % config.__name__) + return + + config.register_launcher_actions() + + +def register_environment_actions(): + """Register actions from AVALON_ACTIONS for Launcher.""" + + paths = os.environ.get("AVALON_ACTIONS") + if not paths: + return + + for path in paths.split(os.pathsep): + api.register_plugin_path(api.Action, path) + + # Run "register" if found. + for module in lib.modules_from_path(path): + if "register" not in dir(module): + continue + + try: + module.register() + except Exception as e: + print( + "Register method in {0} failed: {1}".format( + module, str(e) + ) + ) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py new file mode 100644 index 0000000000..8bce705fc9 --- /dev/null +++ b/pype/tools/launcher/app.py @@ -0,0 +1,717 @@ +import sys +import copy + +from avalon.vendor.Qt import QtWidgets, QtCore, QtGui +from avalon import io, api, style + +from avalon.tools import lib as tools_lib +from avalon.tools.widgets import AssetWidget +from avalon.vendor import qtawesome +from .models import ProjectModel +from .widgets import ( + ProjectBar, ActionBar, TasksWidget, ActionHistory, SlidePageWidget +) + +from .flickcharm import FlickCharm + +module = sys.modules[__name__] +module.window = None + + +class IconListView(QtWidgets.QListView): + """Styled ListView that allows to toggle between icon and list mode. + + Toggling between the two modes is done by Right Mouse Click. + + """ + + IconMode = 0 + ListMode = 1 + + def __init__(self, parent=None, mode=ListMode): + super(IconListView, self).__init__(parent=parent) + + # Workaround for scrolling being super slow or fast when + # toggling between the two visual modes + self.setVerticalScrollMode(self.ScrollPerPixel) + + self._mode = 0 + self.set_mode(mode) + + def set_mode(self, mode): + if mode == self.IconMode: + self.setViewMode(QtWidgets.QListView.IconMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(True) + self.setWordWrap(True) + self.setGridSize(QtCore.QSize(151, 90)) + self.setIconSize(QtCore.QSize(50, 50)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.setStyleSheet(""" + QListView { + font-size: 11px; + border: 0px; + padding: 0px; + margin: 0px; + + } + + QListView::item { + margin-top: 6px; + /* Won't work without borders set */ + border: 0px; + } + + /* For icon only */ + QListView::icon { + top: 3px; + } + """) + + self.verticalScrollBar().setSingleStep(30) + + elif self.ListMode: + self.setStyleSheet("") # clear stylesheet + self.setViewMode(QtWidgets.QListView.ListMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(False) + self.setWordWrap(False) + self.setIconSize(QtCore.QSize(20, 20)) + self.setGridSize(QtCore.QSize(100, 25)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.verticalScrollBar().setSingleStep(33.33) + + self._mode = mode + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.set_mode(int(not self._mode)) + return super(IconListView, self).mousePressEvent(event) + + +class ProjectsPanel(QtWidgets.QWidget): + """Projects Page""" + + project_clicked = QtCore.Signal(str) + + def __init__(self, parent=None): + super(ProjectsPanel, self).__init__(parent=parent) + + layout = QtWidgets.QVBoxLayout(self) + + io.install() + view = IconListView(parent=self) + view.setSelectionMode(QtWidgets.QListView.NoSelection) + flick = FlickCharm(parent=self) + flick.activateOn(view) + model = ProjectModel() + model.hide_invisible = True + model.refresh() + view.setModel(model) + + layout.addWidget(view) + + view.clicked.connect(self.on_clicked) + + self.model = model + self.view = view + + def on_clicked(self, index): + if index.isValid(): + project = index.data(QtCore.Qt.DisplayRole) + self.project_clicked.emit(project) + + +class AssetsPanel(QtWidgets.QWidget): + """Assets page""" + + back_clicked = QtCore.Signal() + + def __init__(self, parent=None): + super(AssetsPanel, self).__init__(parent=parent) + + # project bar + project_bar = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(project_bar) + layout.setSpacing(4) + back = QtWidgets.QPushButton("<") + back.setFixedWidth(25) + back.setFixedHeight(23) + projects = ProjectBar() + projects.layout().setContentsMargins(0, 0, 0, 0) + layout.addWidget(back) + layout.addWidget(projects) + + # assets + _assets_widgets = QtWidgets.QWidget() + _assets_widgets.setContentsMargins(0, 0, 0, 0) + assets_layout = QtWidgets.QVBoxLayout(_assets_widgets) + assets_widgets = AssetWidget() + + # Make assets view flickable + flick = FlickCharm(parent=self) + flick.activateOn(assets_widgets.view) + assets_widgets.view.setVerticalScrollMode( + assets_widgets.view.ScrollPerPixel + ) + assets_layout.addWidget(assets_widgets) + + # tasks + tasks_widgets = TasksWidget() + body = QtWidgets.QSplitter() + body.setContentsMargins(0, 0, 0, 0) + body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + body.setOrientation(QtCore.Qt.Horizontal) + body.addWidget(_assets_widgets) + body.addWidget(tasks_widgets) + body.setStretchFactor(0, 100) + body.setStretchFactor(1, 65) + + # main layout + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(project_bar) + layout.addWidget(body) + + self.data = { + "model": { + "projects": projects, + "assets": assets_widgets, + "tasks": tasks_widgets + }, + } + + # signals + projects.project_changed.connect(self.on_project_changed) + assets_widgets.selection_changed.connect(self.asset_changed) + back.clicked.connect(self.back_clicked) + + # Force initial refresh for the assets since we might not be + # trigging a Project switch if we click the project that was set + # prior to launching the Launcher + # todo: remove this behavior when AVALON_PROJECT is not required + assets_widgets.refresh() + + def set_project(self, project): + + projects = self.data["model"]["projects"] + + before = projects.get_current_project() + projects.set_project(project) + if project == before: + # Force a refresh on the assets if the project hasn't changed + self.data["model"]["assets"].refresh() + + def asset_changed(self): + tools_lib.schedule(self.on_asset_changed, 0.05, + channel="assets") + + def on_project_changed(self): + + project = self.data["model"]["projects"].get_current_project() + + api.Session["AVALON_PROJECT"] = project + self.data["model"]["assets"].refresh() + + # Force asset change callback to ensure tasks are correctly reset + self.asset_changed() + + def on_asset_changed(self): + """Callback on asset selection changed + + This updates the task view. + + """ + + print("Asset changed..") + + tasks = self.data["model"]["tasks"] + assets = self.data["model"]["assets"] + + asset = assets.get_active_asset_document() + if asset: + tasks.set_asset(asset["_id"]) + else: + tasks.set_asset(None) + + def _get_current_session(self): + + tasks = self.data["model"]["tasks"] + assets = self.data["model"]["assets"] + + asset = assets.get_active_asset_document() + session = copy.deepcopy(api.Session) + + # Clear some values that we are about to collect if available + session.pop("AVALON_SILO", None) + session.pop("AVALON_ASSET", None) + session.pop("AVALON_TASK", None) + + if asset: + session["AVALON_ASSET"] = asset["name"] + + silo = asset.get("silo") + if silo: + session["AVALON_SILO"] = silo + + task = tasks.get_current_task() + if task: + session["AVALON_TASK"] = task + + return session + + +class Window(QtWidgets.QDialog): + """Launcher interface""" + + def __init__(self, parent=None): + super(Window, self).__init__(parent) + + self.setWindowTitle("Launcher") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + # Allow minimize + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint + ) + + project_panel = ProjectsPanel() + asset_panel = AssetsPanel() + + pages = SlidePageWidget() + pages.addWidget(project_panel) + pages.addWidget(asset_panel) + + # actions + actions = ActionBar() + + # statusbar + statusbar = QtWidgets.QWidget() + message = QtWidgets.QLabel() + message.setFixedHeight(15) + action_history = ActionHistory() + action_history.setStatusTip("Show Action History") + layout = QtWidgets.QHBoxLayout(statusbar) + layout.addWidget(message) + layout.addWidget(action_history) + + # Vertically split Pages and Actions + body = QtWidgets.QSplitter() + body.setContentsMargins(0, 0, 0, 0) + body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + body.setOrientation(QtCore.Qt.Vertical) + body.addWidget(pages) + body.addWidget(actions) + + # Set useful default sizes and set stretch + # for the pages so that is the only one that + # stretches on UI resize. + body.setStretchFactor(0, 10) + body.setSizes([580, 160]) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + layout.addWidget(statusbar) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + self.data = { + "label": { + "message": message, + }, + "pages": { + "project": project_panel, + "asset": asset_panel + }, + "model": { + "actions": actions, + "action_history": action_history + }, + } + + self.pages = pages + self._page = 0 + + # signals + actions.action_clicked.connect(self.on_action_clicked) + action_history.trigger_history.connect(self.on_history_action) + project_panel.project_clicked.connect(self.on_project_clicked) + asset_panel.back_clicked.connect(self.on_back_clicked) + + # Add some signals to propagate from the asset panel + for signal in [ + asset_panel.data["model"]["projects"].project_changed, + asset_panel.data["model"]["assets"].selection_changed, + asset_panel.data["model"]["tasks"].task_changed + ]: + signal.connect(self.on_session_changed) + + # todo: Simplify this callback connection + asset_panel.data["model"]["projects"].project_changed.connect( + self.on_project_changed + ) + + self.resize(520, 740) + + def set_page(self, page): + + current = self.pages.currentIndex() + if current == page and self._page == page: + return + + direction = "right" if page > current else "left" + self._page = page + self.pages.slide_view(page, direction=direction) + + def refresh(self): + asset = self.data["pages"]["asset"] + asset.data["model"]["assets"].refresh() + self.refresh_actions() + + def echo(self, message): + widget = self.data["label"]["message"] + widget.setText(str(message)) + + QtCore.QTimer.singleShot(5000, lambda: widget.setText("")) + + print(message) + + def on_project_changed(self): + project_name = self.data["pages"]["asset"].data["model"]["projects"].get_current_project() + io.Session["AVALON_PROJECT"] = project_name + + # Update the Action plug-ins available for the current project + actions_model = self.data["model"]["actions"].model + actions_model.discover() + + def on_session_changed(self): + self.refresh_actions() + + def refresh_actions(self, delay=1): + tools_lib.schedule(self.on_refresh_actions, delay) + + def on_project_clicked(self, project): + io.Session["AVALON_PROJECT"] = project + asset_panel = self.data["pages"]["asset"] + asset_panel.data["model"]["projects"].refresh() # Refresh projects + asset_panel.set_project(project) + self.set_page(1) + self.refresh_actions() + + def on_back_clicked(self): + + self.set_page(0) + self.data["pages"]["project"].model.refresh() # Refresh projects + self.refresh_actions() + + def on_refresh_actions(self): + session = self.get_current_session() + + actions = self.data["model"]["actions"] + actions.model.set_session(session) + actions.model.refresh() + + def on_action_clicked(self, action): + self.echo("Running action: %s" % action.name) + self.run_action(action) + + def on_history_action(self, history_data): + action, session = history_data + app = QtWidgets.QApplication.instance() + modifiers = app.keyboardModifiers() + + is_control_down = QtCore.Qt.ControlModifier & modifiers + if is_control_down: + # User is holding control, rerun the action + self.run_action(action, session=session) + else: + # Revert to that "session" location + self.set_session(session) + + def get_current_session(self): + + index = self._page + if index == 1: + # Assets page + return self.data["pages"]["asset"]._get_current_session() + + else: + session = copy.deepcopy(api.Session) + + # Remove some potential invalid session values + # that we know are not set when not browsing in + # a project. + session.pop("AVALON_PROJECT", None) + session.pop("AVALON_ASSET", None) + session.pop("AVALON_SILO", None) + session.pop("AVALON_TASK", None) + + return session + + def run_action(self, action, session=None): + + if session is None: + session = self.get_current_session() + + # Add to history + history = self.data["model"]["action_history"] + history.add_action(action, session) + + # Process the Action + action().process(session) + + def set_session(self, session): + + panel = self.data["pages"]["asset"] + + project = session.get("AVALON_PROJECT") + silo = session.get("AVALON_SILO") + asset = session.get("AVALON_ASSET") + task = session.get("AVALON_TASK") + + if project: + + # Force the "in project" view. + self.pages.slide_view(1, direction="right") + + projects = panel.data["model"]["projects"] + index = projects.view.findText(project) + if index >= 0: + projects.view.setCurrentIndex(index) + + if silo: + panel.data["model"]["assets"].set_silo(silo) + + if asset: + panel.data["model"]["assets"].select_assets([asset]) + + if task: + panel.on_asset_changed() # requires a forced refresh first + panel.data["model"]["tasks"].select_task(task) + + +class Application(QtWidgets.QApplication): + + def __init__(self, *args): + super(Application, self).__init__(*args) + + # Set app icon + icon_path = tools_lib.resource("icons", "png", "avalon-logo-16.png") + icon = QtGui.QIcon(icon_path) + + self.setWindowIcon(icon) + + # Toggles + self.toggles = {"autoHide": False} + + # Timers + keep_visible = QtCore.QTimer(self) + keep_visible.setInterval(100) + keep_visible.setSingleShot(True) + + timers = {"keepVisible": keep_visible} + + tray = QtWidgets.QSystemTrayIcon(icon) + tray.setToolTip("Avalon Launcher") + + # Signals + tray.activated.connect(self.on_tray_activated) + self.aboutToQuit.connect(self.on_quit) + + menu = self.build_menu() + tray.setContextMenu(menu) + tray.show() + + tray.showMessage("Avalon", "Launcher started.") + + # Don't close the app when we close the log window. + # self.setQuitOnLastWindowClosed(False) + + self.focusChanged.connect(self.on_focus_changed) + + window = Window() + window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + + self.timers = timers + self._tray = tray + self._window = window + + # geometry = self.calculate_window_geometry(window) + # window.setGeometry(geometry) + + def show(self): + """Show the primary GUI + + This also activates the window and deals with platform-differences. + + """ + + self._window.show() + self._window.raise_() + self._window.activateWindow() + + self.timers["keepVisible"].start() + + def on_tray_activated(self, reason): + if self._window.isVisible(): + self._window.hide() + + elif reason == QtWidgets.QSystemTrayIcon.Trigger: + self.show() + + def on_focus_changed(self, old, new): + """Respond to window losing focus""" + window = new + keep_visible = self.timers["keepVisible"].isActive() + self._window.hide() if (self.toggles["autoHide"] and + not window and + not keep_visible) else None + + def on_autohide_changed(self, auto_hide): + """Respond to changes to auto-hide + + Auto-hide is changed in the UI and determines whether or not + the UI hides upon losing focus. + + """ + + self.toggles["autoHide"] = auto_hide + self.echo("Hiding when losing focus" if auto_hide else "Stays visible") + + def on_quit(self): + """Respond to the application quitting""" + self._tray.hide() + + def build_menu(self): + """Build the right-mouse context menu for the tray icon""" + menu = QtWidgets.QMenu() + + icon = qtawesome.icon("fa.eye", color=style.colors.default) + open = QtWidgets.QAction(icon, "Open", self) + open.triggered.connect(self.show) + + def toggle(): + self.on_autohide_changed(not self.toggles['autoHide']) + + keep_open = QtWidgets.QAction("Keep open", self) + keep_open.setCheckable(True) + keep_open.setChecked(not self.toggles['autoHide']) + keep_open.triggered.connect(toggle) + + quit = QtWidgets.QAction("Quit", self) + quit.triggered.connect(self.quit) + + menu.setStyleSheet(""" + QMenu { + padding: 0px; + margin: 0px; + } + """) + + for action in [open, keep_open, quit]: + menu.addAction(action) + + return menu + + def calculate_window_geometry(self, window): + """Respond to status changes + + On creation, align window with where the tray icon is + located. For example, if the tray icon is in the upper + right corner of the screen, then this is where the + window is supposed to appear. + + Arguments: + status (int): Provided by Qt, the status flag of + loading the input file. + + """ + + tray_x = self._tray.geometry().x() + tray_y = self._tray.geometry().y() + + width = window.width() + width = max(width, window.minimumWidth()) + + height = window.height() + height = max(height, window.sizeHint().height()) + + desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() + screen_geometry = window.geometry() + + screen_width = screen_geometry.width() + screen_height = screen_geometry.height() + + # Calculate width and height of system tray + systray_width = screen_geometry.width() - desktop_geometry.width() + systray_height = screen_geometry.height() - desktop_geometry.height() + + padding = 10 + + x = screen_width - width + y = screen_height - height + + if tray_x < (screen_width / 2): + x = 0 + systray_width + padding + else: + x -= systray_width + padding + + if tray_y < (screen_height / 2): + y = 0 + systray_height + padding + else: + y -= systray_height + padding + + return QtCore.QRect(x, y, width, height) + + +def show(root=None, debug=False, parent=None): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + parent (QtCore.QObject, optional): When provided parent the interface + to this QObject. + + """ + + app = Application(sys.argv) + app.setStyleSheet(style.load_stylesheet()) + + # Show the window on launch + app.show() + + app.exec_() + + +def cli(args): + import argparse + parser = argparse.ArgumentParser() + #parser.add_argument("project") + + args = parser.parse_args(args) + #project = args.project + + import launcher.actions as actions + print("Registering default actions..") + actions.register_default_actions() + print("Registering config actions..") + actions.register_config_actions() + print("Registering environment actions..") + actions.register_environment_actions() + io.install() + + #api.Session["AVALON_PROJECT"] = project + + import traceback + sys.excepthook = lambda typ, val, tb: traceback.print_last() + + show() diff --git a/pype/tools/launcher/flickcharm.py b/pype/tools/launcher/flickcharm.py new file mode 100644 index 0000000000..b4dd69be6c --- /dev/null +++ b/pype/tools/launcher/flickcharm.py @@ -0,0 +1,305 @@ +""" +This based on the flickcharm-python code from: + https://code.google.com/archive/p/flickcharm-python/ + +Which states: + This is a Python (PyQt) port of Ariya Hidayat's elegant FlickCharm + hack which adds kinetic scrolling to any scrollable Qt widget. + + Licensed under GNU GPL version 2 or later. + +It has been altered to fix edge cases where clicks and drags would not +propagate correctly under some conditions. It also allows a small "dead zone" +threshold in which it will still propagate the user pressed click if he or she +travelled only very slightly with the cursor. + +""" + +import copy +import sys +from Qt import QtWidgets, QtCore, QtGui + + +class FlickData(object): + Steady = 0 + Pressed = 1 + ManualScroll = 2 + AutoScroll = 3 + Stop = 4 + + def __init__(self): + self.state = FlickData.Steady + self.widget = None + self.pressPos = QtCore.QPoint(0, 0) + self.offset = QtCore.QPoint(0, 0) + self.dragPos = QtCore.QPoint(0, 0) + self.speed = QtCore.QPoint(0, 0) + self.travelled = 0 + self.ignored = [] + + +class FlickCharm(QtCore.QObject): + """Make scrollable widgets flickable. + + For example: + charm = FlickCharm() + charm.activateOn(widget) + + It can `activateOn` multiple widgets with a single FlickCharm instance. + Be aware that the FlickCharm object must be kept around for it not + to get garbage collected and losing the flickable behavior. + + Flick away! + + """ + + def __init__(self, parent=None): + super(FlickCharm, self).__init__(parent=parent) + + self.flickData = {} + self.ticker = QtCore.QBasicTimer() + + # The flick button to use + self.button = QtCore.Qt.LeftButton + + # The time taken per update tick of flicking behavior + self.tick_time = 20 + + # Allow a item click/press directly when AutoScroll is slower than + # this threshold velocity + self.click_in_autoscroll_threshold = 10 + + # Allow an item click/press to propagate as opposed to scrolling + # when the cursor travelled less than this amount of pixels + # Note: back & forth motion increases the value too + self.travel_threshold = 20 + + self.max_speed = 64 # max scroll speed + self.drag = 1 # higher drag will stop autoscroll faster + + def activateOn(self, widget): + viewport = widget.viewport() + viewport.installEventFilter(self) + widget.installEventFilter(self) + self.flickData[viewport] = FlickData() + self.flickData[viewport].widget = widget + self.flickData[viewport].state = FlickData.Steady + + def deactivateFrom(self, widget): + + viewport = widget.viewport() + viewport.removeEventFilter(self) + widget.removeEventFilter(self) + self.flickData.pop(viewport) + + def eventFilter(self, obj, event): + + if not obj.isWidgetType(): + return False + + eventType = event.type() + if eventType != QtCore.QEvent.MouseButtonPress and \ + eventType != QtCore.QEvent.MouseButtonRelease and \ + eventType != QtCore.QEvent.MouseMove: + return False + + if event.modifiers() != QtCore.Qt.NoModifier: + return False + + if obj not in self.flickData: + return False + + data = self.flickData[obj] + found, newIgnored = removeAll(data.ignored, event) + if found: + data.ignored = newIgnored + return False + + if data.state == FlickData.Steady: + if eventType == QtCore.QEvent.MouseButtonPress: + if event.buttons() == self.button: + self._set_press_pos_and_offset(event, data) + data.state = FlickData.Pressed + return True + + elif data.state == FlickData.Pressed: + if eventType == QtCore.QEvent.MouseButtonRelease: + # User didn't actually scroll but clicked in + # the widget. Let the original press and release + # event be evaluated on the Widget + data.state = FlickData.Steady + event1 = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, + data.pressPos, + QtCore.Qt.LeftButton, + QtCore.Qt.LeftButton, + QtCore.Qt.NoModifier) + # Copy the current event + event2 = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease, + event.pos(), + event.button(), + event.buttons(), + event.modifiers()) + data.ignored.append(event1) + data.ignored.append(event2) + QtWidgets.QApplication.postEvent(obj, event1) + QtWidgets.QApplication.postEvent(obj, event2) + return True + elif eventType == QtCore.QEvent.MouseMove: + data.state = FlickData.ManualScroll + data.dragPos = QtGui.QCursor.pos() + if not self.ticker.isActive(): + self.ticker.start(self.tick_time, self) + return True + + elif data.state == FlickData.ManualScroll: + if eventType == QtCore.QEvent.MouseMove: + pos = event.pos() + delta = pos - data.pressPos + data.travelled += delta.manhattanLength() + setScrollOffset(data.widget, data.offset - delta) + return True + elif eventType == QtCore.QEvent.MouseButtonRelease: + + if data.travelled <= self.travel_threshold: + # If the user travelled less than the threshold + # don't go into autoscroll mode but assume the user + # intended to click instead + return self._propagate_click(obj, event, data) + + data.state = FlickData.AutoScroll + return True + + elif data.state == FlickData.AutoScroll: + if eventType == QtCore.QEvent.MouseButtonPress: + + # Allow pressing when auto scroll is already slower than + # the click in autoscroll threshold + velocity = data.speed.manhattanLength() + if velocity <= self.click_in_autoscroll_threshold: + self._set_press_pos_and_offset(event, data) + data.state = FlickData.Pressed + else: + data.state = FlickData.Stop + + data.speed = QtCore.QPoint(0, 0) + return True + elif eventType == QtCore.QEvent.MouseButtonRelease: + data.state = FlickData.Steady + data.speed = QtCore.QPoint(0, 0) + return True + + elif data.state == FlickData.Stop: + if eventType == QtCore.QEvent.MouseButtonRelease: + data.state = FlickData.Steady + + # If the user had a very limited scroll smaller than the + # threshold consider it a regular press and release. + if data.travelled < self.travel_threshold: + return self._propagate_click(obj, event, data) + + return True + elif eventType == QtCore.QEvent.MouseMove: + # Reset the press position and offset to allow us to "continue" + # the scroll from the new point the user clicked and then held + # down to continue scrolling after AutoScroll. + self._set_press_pos_and_offset(event, data) + data.state = FlickData.ManualScroll + + data.dragPos = QtGui.QCursor.pos() + if not self.ticker.isActive(): + self.ticker.start(self.tick_time, self) + return True + + return False + + def _set_press_pos_and_offset(self, event, data): + """Store current event position on Press""" + data.state = FlickData.Pressed + data.pressPos = copy.copy(event.pos()) + data.offset = scrollOffset(data.widget) + data.travelled = 0 + + def _propagate_click(self, obj, event, data): + """Propagate from Pressed state with MouseButtonRelease event. + + Use only on button release in certain states to propagate a click, + for example when the user dragged only a slight distance under the + travel threshold. + + """ + + data.state = FlickData.Pressed + data.pressPos = copy.copy(event.pos()) + data.offset = scrollOffset(data.widget) + data.travelled = 0 + self.eventFilter(obj, event) + return True + + def timerEvent(self, event): + + count = 0 + for data in self.flickData.values(): + if data.state == FlickData.ManualScroll: + count += 1 + cursorPos = QtGui.QCursor.pos() + data.speed = cursorPos - data.dragPos + data.dragPos = cursorPos + elif data.state == FlickData.AutoScroll: + count += 1 + data.speed = deaccelerate(data.speed, + a=self.drag, + maxVal=self.max_speed) + p = scrollOffset(data.widget) + new_p = p - data.speed + setScrollOffset(data.widget, new_p) + + if scrollOffset(data.widget) == p: + # If this scroll resulted in no change on the widget + # we reached the end of the list and set the speed to + # zero. + data.speed = QtCore.QPoint(0, 0) + + if data.speed == QtCore.QPoint(0, 0): + data.state = FlickData.Steady + + if count == 0: + self.ticker.stop() + + super(FlickCharm, self).timerEvent(event) + + +def scrollOffset(widget): + x = widget.horizontalScrollBar().value() + y = widget.verticalScrollBar().value() + return QtCore.QPoint(x, y) + + +def setScrollOffset(widget, p): + widget.horizontalScrollBar().setValue(p.x()) + widget.verticalScrollBar().setValue(p.y()) + + +def deaccelerate(speed, a=1, maxVal=64): + + x = max(min(speed.x(), maxVal), -maxVal) + y = max(min(speed.y(), maxVal), -maxVal) + if x > 0: + x = max(0, x - a) + elif x < 0: + x = min(0, x + a) + if y > 0: + y = max(0, y - a) + elif y < 0: + y = min(0, y + a) + return QtCore.QPoint(x, y) + + +def removeAll(list, val): + found = False + ret = [] + for element in list: + if element == val: + found = True + else: + ret.append(element) + return found, ret diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py new file mode 100644 index 0000000000..8cd117074c --- /dev/null +++ b/pype/tools/launcher/lib.py @@ -0,0 +1,67 @@ +"""Utility script for updating database with configuration files + +Until assets are created entirely in the database, this script +provides a bridge between the file-based project inventory and configuration. + +- Migrating an old project: + $ python -m avalon.inventory --extract --silo-parent=f02_prod + $ python -m avalon.inventory --upload + +- Managing an existing project: + 1. Run `python -m avalon.inventory --load` + 2. Update the .inventory.toml or .config.toml + 3. Run `python -m avalon.inventory --save` + +""" + +from avalon import io, lib, pipeline + + +def list_project_tasks(): + """List the project task types available in the current project""" + project = io.find_one({"type": "project"}) + return [task["name"] for task in project["config"]["tasks"]] + + +def get_application_actions(project): + """Define dynamic Application classes for project using `.toml` files + + Args: + project (dict): project document from the database + + Returns: + list: list of dictionaries + """ + + apps = [] + for app in project["config"]["apps"]: + try: + app_name = app["name"] + app_definition = lib.get_application(app_name) + except Exception as exc: + print("Unable to load application: %s - %s" % (app['name'], exc)) + continue + + # Get from app definition, if not there from app in project + icon = app_definition.get("icon", app.get("icon", "folder-o")) + color = app_definition.get("color", app.get("color", None)) + order = app_definition.get("order", app.get("order", 0)) + label = app.get("label") or app_definition.get("label") or app["name"] + group = app.get("group") or app_definition.get("group") + + action = type( + "app_{}".format(app_name), + (pipeline.Application,), + { + "name": app_name, + "label": label, + "group": group, + "icon": icon, + "color": color, + "order": order, + "config": app_definition.copy() + } + ) + + apps.append(action) + return apps diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py new file mode 100644 index 0000000000..17c28c19b3 --- /dev/null +++ b/pype/tools/launcher/models.py @@ -0,0 +1,292 @@ +import os +import copy +import logging +import collections + +from . import lib +from Qt import QtCore, QtGui +from avalon.vendor import qtawesome +from avalon import io, style, api + + +log = logging.getLogger(__name__) + +icons_dir = "C:/Users/iLLiCiT/Desktop/Prace/pype-setup/repos/pype/pype/resources/app_icons" + + +class TaskModel(QtGui.QStandardItemModel): + """A model listing the tasks combined for a list of assets""" + + def __init__(self, parent=None): + super(TaskModel, self).__init__(parent=parent) + self._num_assets = 0 + + self.default_icon = qtawesome.icon( + "fa.male", color=style.colors.default + ) + self.no_task_icon = qtawesome.icon( + "fa.exclamation-circle", color=style.colors.mid + ) + + self._icons = {} + + self._get_task_icons() + + def _get_task_icons(self): + if io.Session.get("AVALON_PROJECT") is None: + return + + # Get the project configured icons from database + project = io.find_one({"type": "project"}) + for task in project["config"].get("tasks") or []: + icon_name = task.get("icon") + if icon_name: + self._icons[task["name"]] = qtawesome.icon( + "fa.{}".format(icon_name), color=style.colors.default + ) + + def set_assets(self, asset_ids=None, asset_docs=None): + """Set assets to track by their database id + + Arguments: + asset_ids (list): List of asset ids. + asset_docs (list): List of asset entities from MongoDB. + + """ + + if asset_docs is None and asset_ids is not None: + # find assets in db by query + asset_docs = list(io.find({ + "type": "asset", + "_id": {"$in": asset_ids} + })) + db_assets_ids = tuple(asset_doc["_id"] for asset_doc in asset_docs) + + # check if all assets were found + not_found = tuple( + str(asset_id) + for asset_id in asset_ids + if asset_id not in db_assets_ids + ) + + assert not not_found, "Assets not found by id: {0}".format( + ", ".join(not_found) + ) + + self.clear() + + if not asset_docs: + return + + task_names = collections.Counter() + for asset_doc in asset_docs: + asset_tasks = asset_doc.get("data", {}).get("tasks", []) + task_names.update(asset_tasks) + + self.beginResetModel() + + if not task_names: + item = QtGui.QStandardItem(self.no_task_icon, "No task") + item.setEnabled(False) + self.appendRow(item) + + else: + for task_name, count in sorted(task_names.items()): + icon = self._icons.get(task_name, self.default_icon) + item = QtGui.QStandardItem(icon, task_name) + self.appendRow(item) + + self.endResetModel() + + def headerData(self, section, orientation, role): + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + and section == 0 + ): + return "Tasks" + return super(TaskModel, self).headerData(section, orientation, role) + + +class ActionModel(QtGui.QStandardItemModel): + ACTION_ROLE = QtCore.Qt.UserRole + GROUP_ROLE = QtCore.Qt.UserRole + 1 + + def __init__(self, parent=None): + super(ActionModel, self).__init__(parent=parent) + self._icon_cache = {} + self._group_icon_cache = {} + self._session = {} + self._groups = {} + self.default_icon = qtawesome.icon("fa.cube", color="white") + # Cache of available actions + self._registered_actions = list() + + self.discover() + + def discover(self): + """Set up Actions cache. Run this for each new project.""" + if io.Session.get("AVALON_PROJECT") is None: + self._registered_actions = list() + return + + # Discover all registered actions + actions = api.discover(api.Action) + + # Get available project actions and the application actions + project_doc = io.find_one({"type": "project"}) + app_actions = lib.get_application_actions(project_doc) + actions.extend(app_actions) + + self._registered_actions = actions + + def get_icon(self, action, skip_default=False): + icon_name = action.icon + if not icon_name: + if skip_default: + return None + return self.default_icon + + icon = self._icon_cache.get(icon_name) + if icon: + return icon + + icon = self.default_icon + icon_path = os.path.join(icons_dir, icon_name) + if os.path.exists(icon_path): + icon = QtGui.QIcon(icon_path) + self._icon_cache[icon_name] = icon + return icon + + try: + icon_color = getattr(action, "color", None) or "white" + icon = qtawesome.icon( + "fa.{}".format(icon_name), color=icon_color + ) + + except Exception: + print("Can't load icon \"{}\"".format(icon_name)) + + self._icon_cache[icon_name] = self.default_icon + return icon + + def refresh(self): + # Validate actions based on compatibility + self.clear() + + self._groups.clear() + + actions = self.filter_compatible_actions(self._registered_actions) + + self.beginResetModel() + + single_actions = [] + grouped_actions = collections.defaultdict(list) + for action in actions: + group_name = getattr(action, "group", None) + if not group_name: + single_actions.append(action) + else: + grouped_actions[group_name].append(action) + + for group_name, actions in tuple(grouped_actions.items()): + if len(actions) == 1: + grouped_actions.pop(group_name) + single_actions.append(actions[0]) + + items_by_order = collections.defaultdict(list) + for action in single_actions: + icon = self.get_icon(action) + item = QtGui.QStandardItem( + icon, str(action.label or action.name) + ) + item.setData(action, self.ACTION_ROLE) + items_by_order[action.order].append(item) + + for group_name, actions in grouped_actions.items(): + icon = None + order = None + for action in actions: + if order is None or action.order < order: + order = action.order + + if icon is None: + _icon = self.get_icon(action) + if _icon: + icon = _icon + + if icon is None: + icon = self.default_icon + + item = QtGui.QStandardItem(icon, group_name) + item.setData(actions, self.ACTION_ROLE) + item.setData(True, self.GROUP_ROLE) + + items_by_order[order].append(item) + + for order in sorted(items_by_order.keys()): + for item in items_by_order[order]: + self.appendRow(item) + + self.endResetModel() + + def set_session(self, session): + assert isinstance(session, dict) + self._session = copy.deepcopy(session) + self.refresh() + + def filter_compatible_actions(self, actions): + """Collect all actions which are compatible with the environment + + Each compatible action will be translated to a dictionary to ensure + the action can be visualized in the launcher. + + Args: + actions (list): list of classes + + Returns: + list: collection of dictionaries sorted on order int he + """ + + compatible = [] + for action in actions: + if action().is_compatible(self._session): + compatible.append(action) + + # Sort by order and name + return sorted( + compatible, + key=lambda action: (action.order, action.name) + ) + + +class ProjectModel(QtGui.QStandardItemModel): + """List of projects""" + + def __init__(self, parent=None): + super(ProjectModel, self).__init__(parent=parent) + + self.hide_invisible = False + self.project_icon = qtawesome.icon("fa.map", color="white") + + def refresh(self): + self.clear() + self.beginResetModel() + + for project_doc in self.get_projects(): + item = QtGui.QStandardItem(self.project_icon, project_doc["name"]) + self.appendRow(item) + + self.endResetModel() + + def get_projects(self): + project_docs = [] + for project_doc in sorted(io.projects(), key=lambda x: x["name"]): + if ( + self.hide_invisible + and not project_doc["data"].get("visible", True) + ): + continue + project_docs.append(project_doc) + + return project_docs diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py new file mode 100644 index 0000000000..c48376ae91 --- /dev/null +++ b/pype/tools/launcher/widgets.py @@ -0,0 +1,419 @@ +import copy +from Qt import QtWidgets, QtCore, QtGui +from avalon.vendor import qtawesome +from avalon import api + +from .models import TaskModel, ActionModel, ProjectModel +from .flickcharm import FlickCharm + + +class ProjectBar(QtWidgets.QWidget): + project_changed = QtCore.Signal(int) + + def __init__(self, parent=None): + super(ProjectBar, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + + self.model = ProjectModel() + self.model.hide_invisible = True + + self.view = QtWidgets.QComboBox() + self.view.setModel(self.model) + self.view.setRootModelIndex(QtCore.QModelIndex()) + + layout.addWidget(self.view) + + self.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Maximum + ) + + # Initialize + self.refresh() + + # Signals + self.view.currentIndexChanged.connect(self.project_changed) + + # Set current project by default if it's set. + project_name = api.Session.get("AVALON_PROJECT") + if project_name: + self.set_project(project_name) + + def get_current_project(self): + return self.view.currentText() + + def set_project(self, project_name): + index = self.view.findText(project_name) + if index >= 0: + self.view.setCurrentIndex(index) + + def refresh(self): + prev_project_name = self.get_current_project() + + # Refresh without signals + self.view.blockSignals(True) + self.model.refresh() + + self.set_project(prev_project_name) + + self.view.blockSignals(False) + + self.project_changed.emit(self.view.currentIndex()) + + +class ActionDelegate(QtWidgets.QStyledItemDelegate): + extender_lines = 2 + extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) + extender_fg = QtGui.QColor(255, 255, 255)#, 160) + + def __init__(self, group_role, *args, **kwargs): + super(ActionDelegate, self).__init__(*args, **kwargs) + self.group_role = group_role + + def paint(self, painter, option, index): + super(ActionDelegate, self).paint(painter, option, index) + is_group = index.data(self.group_role) + if not is_group: + return + + extender_width = int(option.decorationSize.width() / 2) + extender_height = int(option.decorationSize.height() / 2) + + exteder_rect = QtCore.QRectF( + option.rect.x() + (option.rect.width() / 10), + option.rect.y() + (option.rect.height() / 10), + extender_width, + extender_height + ) + path = QtGui.QPainterPath() + path.addRoundedRect(exteder_rect, 2, 2) + + painter.fillPath(path, self.extender_bg_brush) + + painter.setPen(self.extender_fg) + painter.drawPath(path) + + divider = (2 * self.extender_lines) + 1 + line_height = extender_height / divider + line_width = extender_width - (extender_width / 5) + pos_x = exteder_rect.x() + extender_width / 10 + pos_y = exteder_rect.y() + line_height + for _ in range(self.extender_lines): + line_rect = QtCore.QRectF( + pos_x, pos_y, line_width, round(line_height) + ) + painter.fillRect(line_rect, self.extender_fg) + pos_y += 2 * line_height + + +class ActionBar(QtWidgets.QWidget): + """Launcher interface""" + + action_clicked = QtCore.Signal(object) + + def __init__(self, parent=None): + super(ActionBar, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(8, 0, 8, 0) + + view = QtWidgets.QListView(self) + view.setObjectName("ActionView") + view.setViewMode(QtWidgets.QListView.IconMode) + view.setResizeMode(QtWidgets.QListView.Adjust) + view.setSelectionMode(QtWidgets.QListView.NoSelection) + view.setWrapping(True) + view.setGridSize(QtCore.QSize(70, 75)) + view.setIconSize(QtCore.QSize(30, 30)) + view.setSpacing(0) + view.setWordWrap(True) + + model = ActionModel(self) + view.setModel(model) + + delegate = ActionDelegate(model.GROUP_ROLE, self) + view.setItemDelegate(delegate) + + layout.addWidget(view) + + self.model = model + self.view = view + + # Make view flickable + flick = FlickCharm(parent=view) + flick.activateOn(view) + + self.set_row_height(1) + + view.clicked.connect(self.on_clicked) + + def set_row_height(self, rows): + self.setMinimumHeight(rows * 75) + + def on_clicked(self, index): + if index.isValid(): + is_group = action = index.data(self.model.GROUP_ROLE) + if not is_group: + action = index.data(self.model.ACTION_ROLE) + self.action_clicked.emit(action) + return + + menu = QtWidgets.QMenu(self) + actions = index.data(self.model.ACTION_ROLE) + actions_mapping = {} + for action in actions: + menu_action = QtWidgets.QAction(action.label or action.name) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + action = actions_mapping[result] + self.action_clicked.emit(action) + + +class TasksWidget(QtWidgets.QWidget): + """Widget showing active Tasks""" + + task_changed = QtCore.Signal() + selection_mode = ( + QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows + ) + + def __init__(self): + super(TasksWidget, self).__init__() + + view = QtWidgets.QTreeView() + view.setIndentation(0) + model = TaskModel() + view.setModel(model) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(view) + + view.selectionModel().selectionChanged.connect(self.task_changed) + + self.model = model + self.view = view + + self._last_selected_task = None + + def set_asset(self, asset_id): + if asset_id is None: + # Asset deselected + self.model.set_assets() + return + + # Try and preserve the last selected task and reselect it + # after switching assets. If there's no currently selected + # asset keep whatever the "last selected" was prior to it. + current = self.get_current_task() + if current: + self._last_selected_task = current + + self.model.set_assets([asset_id]) + + if self._last_selected_task: + self.select_task(self._last_selected_task) + + # Force a task changed emit. + self.task_changed.emit() + + def select_task(self, task_name): + """Select a task by name. + + If the task does not exist in the current model then selection is only + cleared. + + Args: + task (str): Name of the task to select. + + """ + + # Clear selection + self.view.selectionModel().clearSelection() + + # Select the task + for row in range(self.model.rowCount()): + index = self.model.index(row, 0) + _task_name = index.data(QtCore.Qt.DisplayRole) + if _task_name == task_name: + self.view.selectionModel().select(index, self.selection_mode) + # Set the currently active index + self.view.setCurrentIndex(index) + break + + def get_current_task(self): + """Return name of task at current index (selected) + + Returns: + str: Name of the current task. + + """ + index = self.view.currentIndex() + if self.view.selectionModel().isSelected(index): + return index.data(QtCore.Qt.DisplayRole) + + +class ActionHistory(QtWidgets.QPushButton): + trigger_history = QtCore.Signal(tuple) + + def __init__(self, parent=None): + super(ActionHistory, self).__init__(parent=parent) + + self.max_history = 15 + + self.setFixedWidth(25) + self.setFixedHeight(25) + + self.setIcon(qtawesome.icon("fa.history", color="#CCCCCC")) + self.setIconSize(QtCore.QSize(15, 15)) + + self._history = [] + self.clicked.connect(self.show_history) + + def show_history(self): + # Show history popup + if not self._history: + return + + point = QtGui.QCursor().pos() + + widget = QtWidgets.QListWidget() + widget.setSelectionMode(widget.NoSelection) + + widget.setStyleSheet(""" + * { + font-family: "Courier New"; + } + """) + + largest_label_num_chars = 0 + largest_action_label = max(len(x[0].label) for x in self._history) + action_session_role = QtCore.Qt.UserRole + 1 + + for action, session in reversed(self._history): + project = session.get("AVALON_PROJECT") + asset = session.get("AVALON_ASSET") + task = session.get("AVALON_TASK") + breadcrumb = " > ".join(x for x in [project, asset, task] if x) + + m = "{{action:{0}}} | {{breadcrumb}}".format(largest_action_label) + label = m.format(action=action.label, breadcrumb=breadcrumb) + + icon_name = action.icon + color = action.color or "white" + icon = qtawesome.icon("fa.%s" % icon_name, color=color) + item = QtWidgets.QListWidgetItem(icon, label) + item.setData(action_session_role, (action, session)) + + largest_label_num_chars = max(largest_label_num_chars, len(label)) + + widget.addItem(item) + + # Show history + width = 40 + (largest_label_num_chars * 7) # padding + icon + text + entry_height = 21 + height = entry_height * len(self._history) + + dialog = QtWidgets.QDialog(parent=self) + dialog.setWindowTitle("Action History") + dialog.setWindowFlags(QtCore.Qt.FramelessWindowHint | + QtCore.Qt.Popup) + dialog.setSizePolicy(QtWidgets.QSizePolicy.Ignored, + QtWidgets.QSizePolicy.Ignored) + + layout = QtWidgets.QVBoxLayout(dialog) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(widget) + + def on_clicked(index): + data = index.data(action_session_role) + self.trigger_history.emit(data) + dialog.close() + + widget.clicked.connect(on_clicked) + + dialog.setGeometry(point.x() - width, + point.y() - height, + width, + height) + dialog.exec_() + + self.widget_popup = widget + + def add_action(self, action, session): + key = (action, copy.deepcopy(session)) + + # Remove entry if already exists + try: + index = self._history.index(key) + self._history.pop(index) + except ValueError: + pass + + self._history.append(key) + + # Slice the end of the list if we exceed the max history + if len(self._history) > self.max_history: + self._history = self._history[-self.max_history:] + + def clear_history(self): + self._history[:] = [] + + +class SlidePageWidget(QtWidgets.QStackedWidget): + """Stacked widget that nicely slides between its pages""" + + directions = { + "left": QtCore.QPoint(-1, 0), + "right": QtCore.QPoint(1, 0), + "up": QtCore.QPoint(0, 1), + "down": QtCore.QPoint(0, -1) + } + + def slide_view(self, index, direction="right"): + + if self.currentIndex() == index: + return + + offset = self.directions.get(direction) + assert offset is not None, "invalid slide direction: %s" % (direction,) + + width = self.frameRect().width() + height = self.frameRect().height() + offset = QtCore.QPoint(offset.x() * width, offset.y() * height) + + new_page = self.widget(index) + new_page.setGeometry(0, 0, width, height) + curr_pos = new_page.pos() + new_page.move(curr_pos + offset) + new_page.show() + new_page.raise_() + + current_page = self.currentWidget() + + b_pos = QtCore.QByteArray(b"pos") + + anim_old = QtCore.QPropertyAnimation(current_page, b_pos, self) + anim_old.setDuration(250) + anim_old.setStartValue(curr_pos) + anim_old.setEndValue(curr_pos - offset) + anim_old.setEasingCurve(QtCore.QEasingCurve.OutQuad) + + anim_new = QtCore.QPropertyAnimation(new_page, b_pos, self) + anim_new.setDuration(250) + 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(): + self.setCurrentWidget(new_page) + + anim_group.finished.connect(slide_finished) + anim_group.start() From 1a13997eb9c3edcb495b5c4149b23dec54eeaf93 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 21:53:13 +0200 Subject: [PATCH 02/31] use resources --- pype/tools/launcher/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 17c28c19b3..ae29c65297 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -7,12 +7,10 @@ from . import lib from Qt import QtCore, QtGui from avalon.vendor import qtawesome from avalon import io, style, api - +from pype.api import resources log = logging.getLogger(__name__) -icons_dir = "C:/Users/iLLiCiT/Desktop/Prace/pype-setup/repos/pype/pype/resources/app_icons" - class TaskModel(QtGui.QStandardItemModel): """A model listing the tasks combined for a list of assets""" @@ -152,7 +150,7 @@ class ActionModel(QtGui.QStandardItemModel): return icon icon = self.default_icon - icon_path = os.path.join(icons_dir, icon_name) + icon_path = resources.get_resource(icon_name) if os.path.exists(icon_path): icon = QtGui.QIcon(icon_path) self._icon_cache[icon_name] = icon From 80ce7dc738d3af53c5002ba0f8cc650fb2e412a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 22:02:38 +0200 Subject: [PATCH 03/31] replaced `<` with icon --- pype/tools/launcher/app.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 8bce705fc9..9cef313bf5 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -138,8 +138,11 @@ class AssetsPanel(QtWidgets.QWidget): project_bar = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(project_bar) layout.setSpacing(4) - back = QtWidgets.QPushButton("<") - back.setFixedWidth(25) + + icon = qtawesome.icon("fa.angle-left", color="white") + back = QtWidgets.QPushButton() + back.setIcon(icon) + back.setFixedWidth(23) back.setFixedHeight(23) projects = ProjectBar() projects.layout().setContentsMargins(0, 0, 0, 0) @@ -147,9 +150,9 @@ class AssetsPanel(QtWidgets.QWidget): layout.addWidget(projects) # assets - _assets_widgets = QtWidgets.QWidget() - _assets_widgets.setContentsMargins(0, 0, 0, 0) - assets_layout = QtWidgets.QVBoxLayout(_assets_widgets) + assets_proxy_widgets = QtWidgets.QWidget() + assets_proxy_widgets.setContentsMargins(0, 0, 0, 0) + assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets) assets_widgets = AssetWidget() # Make assets view flickable @@ -167,7 +170,7 @@ class AssetsPanel(QtWidgets.QWidget): body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) body.setOrientation(QtCore.Qt.Horizontal) - body.addWidget(_assets_widgets) + body.addWidget(assets_proxy_widgets) body.addWidget(tasks_widgets) body.setStretchFactor(0, 100) body.setStretchFactor(1, 65) From 18a1a5d6798dc0bf228a159bf3c299b0adce9c02 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:44:52 +0200 Subject: [PATCH 04/31] fix project check --- pype/tools/launcher/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index ae29c65297..7ad161236f 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -124,7 +124,7 @@ class ActionModel(QtGui.QStandardItemModel): def discover(self): """Set up Actions cache. Run this for each new project.""" - if io.Session.get("AVALON_PROJECT") is None: + if not io.Session.get("AVALON_PROJECT"): self._registered_actions = list() return From 6ea3c492d160cd80b98cd69feb0337d01cd573d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:45:07 +0200 Subject: [PATCH 05/31] stylesheet in code was moved to css --- pype/tools/launcher/app.py | 46 +++++++++++----------------------- pype/tools/launcher/widgets.py | 7 +++--- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 9cef313bf5..abc350641b 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -34,11 +34,17 @@ class IconListView(QtWidgets.QListView): # Workaround for scrolling being super slow or fast when # toggling between the two visual modes self.setVerticalScrollMode(self.ScrollPerPixel) + self.setObjectName("IconView") - self._mode = 0 + self._mode = None self.set_mode(mode) def set_mode(self, mode): + if mode == self._mode: + return + + self._mode = mode + if mode == self.IconMode: self.setViewMode(QtWidgets.QListView.IconMode) self.setResizeMode(QtWidgets.QListView.Adjust) @@ -49,31 +55,15 @@ class IconListView(QtWidgets.QListView): self.setSpacing(0) self.setAlternatingRowColors(False) - self.setStyleSheet(""" - QListView { - font-size: 11px; - border: 0px; - padding: 0px; - margin: 0px; - - } - - QListView::item { - margin-top: 6px; - /* Won't work without borders set */ - border: 0px; - } - - /* For icon only */ - QListView::icon { - top: 3px; - } - """) + self.setProperty("mode", "icon") + self.style().polish(self) self.verticalScrollBar().setSingleStep(30) elif self.ListMode: - self.setStyleSheet("") # clear stylesheet + self.setProperty("mode", "list") + self.style().polish(self) + self.setViewMode(QtWidgets.QListView.ListMode) self.setResizeMode(QtWidgets.QListView.Adjust) self.setWrapping(False) @@ -85,8 +75,6 @@ class IconListView(QtWidgets.QListView): self.verticalScrollBar().setSingleStep(33.33) - self._mode = mode - def mousePressEvent(self, event): if event.button() == QtCore.Qt.RightButton: self.set_mode(int(not self._mode)) @@ -145,7 +133,6 @@ class AssetsPanel(QtWidgets.QWidget): back.setFixedWidth(23) back.setFixedHeight(23) projects = ProjectBar() - projects.layout().setContentsMargins(0, 0, 0, 0) layout.addWidget(back) layout.addWidget(projects) @@ -216,10 +203,8 @@ class AssetsPanel(QtWidgets.QWidget): channel="assets") def on_project_changed(self): - - project = self.data["model"]["projects"].get_current_project() - - api.Session["AVALON_PROJECT"] = project + project_name = self.data["model"]["projects"].get_current_project() + api.Session["AVALON_PROJECT"] = project_name self.data["model"]["assets"].refresh() # Force asset change callback to ensure tasks are correctly reset @@ -392,8 +377,7 @@ class Window(QtWidgets.QDialog): io.Session["AVALON_PROJECT"] = project_name # Update the Action plug-ins available for the current project - actions_model = self.data["model"]["actions"].model - actions_model.discover() + self.data["model"]["actions"].model.discover() def on_session_changed(self): self.refresh_actions() diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index c48376ae91..f8f4e17691 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -13,8 +13,6 @@ class ProjectBar(QtWidgets.QWidget): def __init__(self, parent=None): super(ProjectBar, self).__init__(parent) - layout = QtWidgets.QHBoxLayout(self) - self.model = ProjectModel() self.model.hide_invisible = True @@ -22,6 +20,8 @@ class ProjectBar(QtWidgets.QWidget): self.view.setModel(self.model) self.view.setRootModelIndex(QtCore.QModelIndex()) + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.view) self.setSizePolicy( @@ -119,7 +119,8 @@ class ActionBar(QtWidgets.QWidget): layout.setContentsMargins(8, 0, 8, 0) view = QtWidgets.QListView(self) - view.setObjectName("ActionView") + view.setProperty("mode", "icon") + view.setObjectName("IconView") view.setViewMode(QtWidgets.QListView.IconMode) view.setResizeMode(QtWidgets.QListView.Adjust) view.setSelectionMode(QtWidgets.QListView.NoSelection) From 22a0c4528e90fe7379c3f48cb1519a94ae7faec6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:49:38 +0200 Subject: [PATCH 06/31] ProjectBar made clear in variable names --- pype/tools/launcher/widgets.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index f8f4e17691..f87b16ecc6 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -16,13 +16,13 @@ class ProjectBar(QtWidgets.QWidget): self.model = ProjectModel() self.model.hide_invisible = True - self.view = QtWidgets.QComboBox() - self.view.setModel(self.model) - self.view.setRootModelIndex(QtCore.QModelIndex()) + self.project_combobox = QtWidgets.QComboBox() + self.project_combobox.setModel(self.model) + self.project_combobox.setRootModelIndex(QtCore.QModelIndex()) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.view) + layout.addWidget(self.project_combobox) self.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, @@ -33,7 +33,7 @@ class ProjectBar(QtWidgets.QWidget): self.refresh() # Signals - self.view.currentIndexChanged.connect(self.project_changed) + self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. project_name = api.Session.get("AVALON_PROJECT") @@ -41,25 +41,25 @@ class ProjectBar(QtWidgets.QWidget): self.set_project(project_name) def get_current_project(self): - return self.view.currentText() + return self.project_combobox.currentText() def set_project(self, project_name): - index = self.view.findText(project_name) + index = self.project_combobox.findText(project_name) if index >= 0: - self.view.setCurrentIndex(index) + self.project_combobox.setCurrentIndex(index) def refresh(self): prev_project_name = self.get_current_project() # Refresh without signals - self.view.blockSignals(True) - self.model.refresh() + self.project_combobox.blockSignals(True) + self.model.refresh() self.set_project(prev_project_name) - self.view.blockSignals(False) + self.project_combobox.blockSignals(False) - self.project_changed.emit(self.view.currentIndex()) + self.project_changed.emit(self.project_combobox.currentIndex()) class ActionDelegate(QtWidgets.QStyledItemDelegate): From 8f547489da0ef31790c774205b2c93911b6cec90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:50:00 +0200 Subject: [PATCH 07/31] moved ActionDelegate to delegates.py --- pype/tools/launcher/delegates.py | 46 ++++++++++++++++++++++++++++++++ pype/tools/launcher/widgets.py | 46 +------------------------------- 2 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 pype/tools/launcher/delegates.py diff --git a/pype/tools/launcher/delegates.py b/pype/tools/launcher/delegates.py new file mode 100644 index 0000000000..750301cec4 --- /dev/null +++ b/pype/tools/launcher/delegates.py @@ -0,0 +1,46 @@ +from Qt import QtCore, QtWidgets, QtGui + + +class ActionDelegate(QtWidgets.QStyledItemDelegate): + extender_lines = 2 + extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) + extender_fg = QtGui.QColor(255, 255, 255)#, 160) + + def __init__(self, group_role, *args, **kwargs): + super(ActionDelegate, self).__init__(*args, **kwargs) + self.group_role = group_role + + def paint(self, painter, option, index): + super(ActionDelegate, self).paint(painter, option, index) + is_group = index.data(self.group_role) + if not is_group: + return + + extender_width = int(option.decorationSize.width() / 2) + extender_height = int(option.decorationSize.height() / 2) + + exteder_rect = QtCore.QRectF( + option.rect.x() + (option.rect.width() / 10), + option.rect.y() + (option.rect.height() / 10), + extender_width, + extender_height + ) + path = QtGui.QPainterPath() + path.addRoundedRect(exteder_rect, 2, 2) + + painter.fillPath(path, self.extender_bg_brush) + + painter.setPen(self.extender_fg) + painter.drawPath(path) + + divider = (2 * self.extender_lines) + 1 + line_height = extender_height / divider + line_width = extender_width - (extender_width / 5) + pos_x = exteder_rect.x() + extender_width / 10 + pos_y = exteder_rect.y() + line_height + for _ in range(self.extender_lines): + line_rect = QtCore.QRectF( + pos_x, pos_y, line_width, round(line_height) + ) + painter.fillRect(line_rect, self.extender_fg) + pos_y += 2 * line_height diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index f87b16ecc6..a32548154b 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome from avalon import api +from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm @@ -62,51 +63,6 @@ class ProjectBar(QtWidgets.QWidget): self.project_changed.emit(self.project_combobox.currentIndex()) -class ActionDelegate(QtWidgets.QStyledItemDelegate): - extender_lines = 2 - extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) - extender_fg = QtGui.QColor(255, 255, 255)#, 160) - - def __init__(self, group_role, *args, **kwargs): - super(ActionDelegate, self).__init__(*args, **kwargs) - self.group_role = group_role - - def paint(self, painter, option, index): - super(ActionDelegate, self).paint(painter, option, index) - is_group = index.data(self.group_role) - if not is_group: - return - - extender_width = int(option.decorationSize.width() / 2) - extender_height = int(option.decorationSize.height() / 2) - - exteder_rect = QtCore.QRectF( - option.rect.x() + (option.rect.width() / 10), - option.rect.y() + (option.rect.height() / 10), - extender_width, - extender_height - ) - path = QtGui.QPainterPath() - path.addRoundedRect(exteder_rect, 2, 2) - - painter.fillPath(path, self.extender_bg_brush) - - painter.setPen(self.extender_fg) - painter.drawPath(path) - - divider = (2 * self.extender_lines) + 1 - line_height = extender_height / divider - line_width = extender_width - (extender_width / 5) - pos_x = exteder_rect.x() + extender_width / 10 - pos_y = exteder_rect.y() + line_height - for _ in range(self.extender_lines): - line_rect = QtCore.QRectF( - pos_x, pos_y, line_width, round(line_height) - ) - painter.fillRect(line_rect, self.extender_fg) - pos_y += 2 * line_height - - class ActionBar(QtWidgets.QWidget): """Launcher interface""" From 0b83ea44e14be6ad21a66eaf0d71645b3f07de12 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:52:40 +0200 Subject: [PATCH 08/31] cleared action bar --- pype/tools/launcher/widgets.py | 36 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index a32548154b..dedc3a5f50 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -109,25 +109,27 @@ class ActionBar(QtWidgets.QWidget): self.setMinimumHeight(rows * 75) def on_clicked(self, index): - if index.isValid(): - is_group = action = index.data(self.model.GROUP_ROLE) - if not is_group: - action = index.data(self.model.ACTION_ROLE) - self.action_clicked.emit(action) - return + if not index.isValid(): + return - menu = QtWidgets.QMenu(self) - actions = index.data(self.model.ACTION_ROLE) - actions_mapping = {} - for action in actions: - menu_action = QtWidgets.QAction(action.label or action.name) - menu.addAction(menu_action) - actions_mapping[menu_action] = action + is_group = index.data(self.model.GROUP_ROLE) + if not is_group: + action = index.data(self.model.ACTION_ROLE) + self.action_clicked.emit(action) + return - result = menu.exec_(QtGui.QCursor.pos()) - if result: - action = actions_mapping[result] - self.action_clicked.emit(action) + menu = QtWidgets.QMenu(self) + actions = index.data(self.model.ACTION_ROLE) + actions_mapping = {} + for action in actions: + menu_action = QtWidgets.QAction(action.label or action.name) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + action = actions_mapping[result] + self.action_clicked.emit(action) class TasksWidget(QtWidgets.QWidget): From ad02f48d29103c3810271e668c2ab4d1c1c74963 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:56:54 +0200 Subject: [PATCH 09/31] made clear variable names --- pype/tools/launcher/widgets.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index dedc3a5f50..37c636423f 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -333,16 +333,20 @@ class SlidePageWidget(QtWidgets.QStackedWidget): } def slide_view(self, index, direction="right"): - if self.currentIndex() == index: return - offset = self.directions.get(direction) - assert offset is not None, "invalid slide direction: %s" % (direction,) + offset_direction = self.directions.get(direction) + if offset_direction is None: + print("BUG: invalid slide direction: {}".format(direction)) + return width = self.frameRect().width() height = self.frameRect().height() - offset = QtCore.QPoint(offset.x() * width, offset.y() * height) + offset = QtCore.QPoint( + offset_direction.x() * width, + offset_direction.y() * height + ) new_page = self.widget(index) new_page.setGeometry(0, 0, width, height) From 40ade937d42677ac233ee03827b7ee8aa6110f22 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 22:23:03 +0200 Subject: [PATCH 10/31] organization changes --- pype/tools/launcher/widgets.py | 43 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 37c636423f..a264466dbc 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -237,11 +237,8 @@ class ActionHistory(QtWidgets.QPushButton): if not self._history: return - point = QtGui.QCursor().pos() - widget = QtWidgets.QListWidget() widget.setSelectionMode(widget.NoSelection) - widget.setStyleSheet(""" * { font-family: "Courier New"; @@ -272,16 +269,15 @@ class ActionHistory(QtWidgets.QPushButton): widget.addItem(item) # Show history - width = 40 + (largest_label_num_chars * 7) # padding + icon + text - entry_height = 21 - height = entry_height * len(self._history) - dialog = QtWidgets.QDialog(parent=self) dialog.setWindowTitle("Action History") - dialog.setWindowFlags(QtCore.Qt.FramelessWindowHint | - QtCore.Qt.Popup) - dialog.setSizePolicy(QtWidgets.QSizePolicy.Ignored, - QtWidgets.QSizePolicy.Ignored) + dialog.setWindowFlags( + QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup + ) + dialog.setSizePolicy( + QtWidgets.QSizePolicy.Ignored, + QtWidgets.QSizePolicy.Ignored + ) layout = QtWidgets.QVBoxLayout(dialog) layout.setContentsMargins(0, 0, 0, 0) @@ -294,10 +290,18 @@ class ActionHistory(QtWidgets.QPushButton): widget.clicked.connect(on_clicked) - dialog.setGeometry(point.x() - width, - point.y() - height, - width, - height) + # padding + icon + text + width = 40 + (largest_label_num_chars * 7) + entry_height = 21 + height = entry_height * len(self._history) + + point = QtGui.QCursor().pos() + dialog.setGeometry( + point.x() - width, + point.y() - height, + width, + height + ) dialog.exec_() self.widget_popup = widget @@ -306,11 +310,8 @@ class ActionHistory(QtWidgets.QPushButton): key = (action, copy.deepcopy(session)) # Remove entry if already exists - try: - index = self._history.index(key) - self._history.pop(index) - except ValueError: - pass + if key in self._history: + self._history.remove(key) self._history.append(key) @@ -319,7 +320,7 @@ class ActionHistory(QtWidgets.QPushButton): self._history = self._history[-self.max_history:] def clear_history(self): - self._history[:] = [] + self._history.clear() class SlidePageWidget(QtWidgets.QStackedWidget): From 92122bbe03bdffe43a712691bc9f0df8e254b2aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 22:23:14 +0200 Subject: [PATCH 11/31] moved icon caching to lib --- pype/tools/launcher/lib.py | 38 +++++++++++++++++++++++++++++++++++ pype/tools/launcher/models.py | 31 ++-------------------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 8cd117074c..033ac33d66 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -14,7 +14,14 @@ provides a bridge between the file-based project inventory and configuration. """ +import os +from Qt import QtGui from avalon import io, lib, pipeline +from avalon.vendor import qtawesome +from pype.api import resources + +ICON_CACHE = {} +NOT_FOUND = type("NotFound", (object, ), {}) def list_project_tasks(): @@ -65,3 +72,34 @@ def get_application_actions(project): apps.append(action) return apps + + +def get_action_icon(self, action, skip_default=False): + icon_name = action.icon + if not icon_name: + return None + + global ICON_CACHE + + icon = ICON_CACHE.get(icon_name) + if icon is NOT_FOUND: + return None + elif icon: + return icon + + icon_path = resources.get_resource(icon_name) + if os.path.exists(icon_path): + icon = QtGui.QIcon(icon_path) + ICON_CACHE[icon_name] = icon + return icon + + try: + icon_color = getattr(action, "color", None) or "white" + icon = qtawesome.icon( + "fa.{}".format(icon_name), color=icon_color + ) + + except Exception: + print("Can't load icon \"{}\"".format(icon_name)) + + return icon diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 7ad161236f..ce6e0c722e 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -7,7 +7,6 @@ from . import lib from Qt import QtCore, QtGui from avalon.vendor import qtawesome from avalon import io, style, api -from pype.api import resources log = logging.getLogger(__name__) @@ -112,8 +111,6 @@ class ActionModel(QtGui.QStandardItemModel): def __init__(self, parent=None): super(ActionModel, self).__init__(parent=parent) - self._icon_cache = {} - self._group_icon_cache = {} self._session = {} self._groups = {} self.default_icon = qtawesome.icon("fa.cube", color="white") @@ -139,33 +136,9 @@ class ActionModel(QtGui.QStandardItemModel): self._registered_actions = actions def get_icon(self, action, skip_default=False): - icon_name = action.icon - if not icon_name: - if skip_default: - return None + icon = lib.get_action_icon(action) + if not icon and not skip_default: return self.default_icon - - icon = self._icon_cache.get(icon_name) - if icon: - return icon - - icon = self.default_icon - icon_path = resources.get_resource(icon_name) - if os.path.exists(icon_path): - icon = QtGui.QIcon(icon_path) - self._icon_cache[icon_name] = icon - return icon - - try: - icon_color = getattr(action, "color", None) or "white" - icon = qtawesome.icon( - "fa.{}".format(icon_name), color=icon_color - ) - - except Exception: - print("Can't load icon \"{}\"".format(icon_name)) - - self._icon_cache[icon_name] = self.default_icon return icon def refresh(self): From 1fcb4e836f18f8e9ad6a0a5c160d812f0633c10a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 23:10:40 +0200 Subject: [PATCH 12/31] small tweaks --- pype/tools/launcher/actions.py | 25 ++- pype/tools/launcher/app.py | 270 +++++++++++++----------------- pype/tools/launcher/flickcharm.py | 1 - pype/tools/launcher/lib.py | 2 +- 4 files changed, 137 insertions(+), 161 deletions(-) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index 2a2e2ab0f0..44ba9a3a60 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -15,9 +15,13 @@ class ProjectManagerAction(api.Action): return "AVALON_PROJECT" in session def process(self, session, **kwargs): - return lib.launch(executable="python", - args=["-u", "-m", "avalon.tools.projectmanager", - session['AVALON_PROJECT']]) + return lib.launch( + executable="python", + args=[ + "-u", "-m", "avalon.tools.projectmanager", + session['AVALON_PROJECT'] + ] + ) class LoaderAction(api.Action): @@ -31,9 +35,12 @@ class LoaderAction(api.Action): return "AVALON_PROJECT" in session def process(self, session, **kwargs): - return lib.launch(executable="python", - args=["-u", "-m", "avalon.tools.cbloader", - session['AVALON_PROJECT']]) + return lib.launch( + executable="python", + args=[ + "-u", "-m", "avalon.tools.cbloader", session['AVALON_PROJECT'] + ] + ) class LoaderLibrary(api.Action): @@ -46,8 +53,10 @@ class LoaderLibrary(api.Action): return True def process(self, session, **kwargs): - return lib.launch(executable="python", - args=["-u", "-m", "avalon.tools.libraryloader"]) + return lib.launch( + executable="python", + args=["-u", "-m", "avalon.tools.libraryloader"] + ) def register_default_actions(): diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index abc350641b..78c5406fa8 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -110,55 +110,59 @@ class ProjectsPanel(QtWidgets.QWidget): def on_clicked(self, index): if index.isValid(): - project = index.data(QtCore.Qt.DisplayRole) - self.project_clicked.emit(project) + project_name = index.data(QtCore.Qt.DisplayRole) + self.project_clicked.emit(project_name) class AssetsPanel(QtWidgets.QWidget): """Assets page""" - back_clicked = QtCore.Signal() def __init__(self, parent=None): super(AssetsPanel, self).__init__(parent=parent) # project bar - project_bar = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(project_bar) + project_bar_widget = QtWidgets.QWidget() + + layout = QtWidgets.QHBoxLayout(project_bar_widget) layout.setSpacing(4) - icon = qtawesome.icon("fa.angle-left", color="white") - back = QtWidgets.QPushButton() - back.setIcon(icon) - back.setFixedWidth(23) - back.setFixedHeight(23) - projects = ProjectBar() - layout.addWidget(back) - layout.addWidget(projects) + btn_back_icon = qtawesome.icon("fa.angle-left", color="white") + btn_back = QtWidgets.QPushButton() + btn_back.setIcon(btn_back_icon) + btn_back.setFixedWidth(23) + btn_back.setFixedHeight(23) + + project_bar = ProjectBar() + + layout.addWidget(btn_back) + layout.addWidget(project_bar) # assets assets_proxy_widgets = QtWidgets.QWidget() assets_proxy_widgets.setContentsMargins(0, 0, 0, 0) assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets) - assets_widgets = AssetWidget() + assets_widget = AssetWidget() # Make assets view flickable flick = FlickCharm(parent=self) - flick.activateOn(assets_widgets.view) - assets_widgets.view.setVerticalScrollMode( - assets_widgets.view.ScrollPerPixel + flick.activateOn(assets_widget.view) + assets_widget.view.setVerticalScrollMode( + assets_widget.view.ScrollPerPixel ) - assets_layout.addWidget(assets_widgets) + assets_layout.addWidget(assets_widget) # tasks - tasks_widgets = TasksWidget() + tasks_widget = TasksWidget() body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) - body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) body.setOrientation(QtCore.Qt.Horizontal) body.addWidget(assets_proxy_widgets) - body.addWidget(tasks_widgets) + body.addWidget(tasks_widget) body.setStretchFactor(0, 100) body.setStretchFactor(1, 65) @@ -166,49 +170,38 @@ class AssetsPanel(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) - layout.addWidget(project_bar) + layout.addWidget(project_bar_widget) layout.addWidget(body) - self.data = { - "model": { - "projects": projects, - "assets": assets_widgets, - "tasks": tasks_widgets - }, - } + self.project_bar = project_bar + self.assets_widget = assets_widget + self.tasks_widget = tasks_widget # signals - projects.project_changed.connect(self.on_project_changed) - assets_widgets.selection_changed.connect(self.asset_changed) - back.clicked.connect(self.back_clicked) + project_bar.project_changed.connect(self.on_project_changed) + assets_widget.selection_changed.connect(self.on_asset_changed) + btn_back.clicked.connect(self.back_clicked) # Force initial refresh for the assets since we might not be # trigging a Project switch if we click the project that was set # prior to launching the Launcher # todo: remove this behavior when AVALON_PROJECT is not required - assets_widgets.refresh() + assets_widget.refresh() def set_project(self, project): - - projects = self.data["model"]["projects"] - - before = projects.get_current_project() - projects.set_project(project) + before = self.project_bar.get_current_project() + self.project_bar.set_project(project) if project == before: # Force a refresh on the assets if the project hasn't changed - self.data["model"]["assets"].refresh() - - def asset_changed(self): - tools_lib.schedule(self.on_asset_changed, 0.05, - channel="assets") + self.assets_widget.refresh() def on_project_changed(self): - project_name = self.data["model"]["projects"].get_current_project() + project_name = self.project_bar.get_current_project() api.Session["AVALON_PROJECT"] = project_name - self.data["model"]["assets"].refresh() + self.assets_widget.refresh() # Force asset change callback to ensure tasks are correctly reset - self.asset_changed() + tools_lib.schedule(self.on_asset_changed, 0.05, channel="assets") def on_asset_changed(self): """Callback on asset selection changed @@ -219,21 +212,14 @@ class AssetsPanel(QtWidgets.QWidget): print("Asset changed..") - tasks = self.data["model"]["tasks"] - assets = self.data["model"]["assets"] - - asset = assets.get_active_asset_document() - if asset: - tasks.set_asset(asset["_id"]) + asset_doc = self.assets_widget.get_active_asset_document() + if asset_doc: + self.tasks_widget.set_asset(asset_doc["_id"]) else: - tasks.set_asset(None) + self.tasks_widget.set_asset(None) - def _get_current_session(self): - - tasks = self.data["model"]["tasks"] - assets = self.data["model"]["assets"] - - asset = assets.get_active_asset_document() + def get_current_session(self): + asset_doc = self.assets_widget.get_active_asset_document() session = copy.deepcopy(api.Session) # Clear some values that we are about to collect if available @@ -241,16 +227,11 @@ class AssetsPanel(QtWidgets.QWidget): session.pop("AVALON_ASSET", None) session.pop("AVALON_TASK", None) - if asset: - session["AVALON_ASSET"] = asset["name"] - - silo = asset.get("silo") - if silo: - session["AVALON_SILO"] = silo - - task = tasks.get_current_task() - if task: - session["AVALON_TASK"] = task + if asset_doc: + session["AVALON_ASSET"] = asset_doc["name"] + task_name = self.tasks_widget.get_current_task() + if task_name: + session["AVALON_TASK"] = task_name return session @@ -273,21 +254,24 @@ class Window(QtWidgets.QDialog): project_panel = ProjectsPanel() asset_panel = AssetsPanel() - pages = SlidePageWidget() - pages.addWidget(project_panel) - pages.addWidget(asset_panel) + page_slider = SlidePageWidget() + page_slider.addWidget(project_panel) + page_slider.addWidget(asset_panel) # actions - actions = ActionBar() + actions_bar = ActionBar() # statusbar statusbar = QtWidgets.QWidget() - message = QtWidgets.QLabel() - message.setFixedHeight(15) + layout = QtWidgets.QHBoxLayout(statusbar) + + message_label = QtWidgets.QLabel() + message_label.setFixedHeight(15) + action_history = ActionHistory() action_history.setStatusTip("Show Action History") - layout = QtWidgets.QHBoxLayout(statusbar) - layout.addWidget(message) + + layout.addWidget(message_label) layout.addWidget(action_history) # Vertically split Pages and Actions @@ -296,8 +280,8 @@ class Window(QtWidgets.QDialog): body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) body.setOrientation(QtCore.Qt.Vertical) - body.addWidget(pages) - body.addWidget(actions) + body.addWidget(page_slider) + body.addWidget(actions_bar) # Set useful default sizes and set stretch # for the pages so that is the only one that @@ -311,73 +295,71 @@ class Window(QtWidgets.QDialog): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) + self.message_label = message_label + self.project_panel = project_panel + self.asset_panel = asset_panel + self.actions_bar = actions_bar + self.action_history = action_history + self.data = { - "label": { - "message": message, - }, "pages": { "project": project_panel, "asset": asset_panel }, "model": { - "actions": actions, + "actions": actions_bar, "action_history": action_history }, } - self.pages = pages + self.page_slider = page_slider self._page = 0 # signals - actions.action_clicked.connect(self.on_action_clicked) + actions_bar.action_clicked.connect(self.on_action_clicked) action_history.trigger_history.connect(self.on_history_action) project_panel.project_clicked.connect(self.on_project_clicked) asset_panel.back_clicked.connect(self.on_back_clicked) # Add some signals to propagate from the asset panel - for signal in [ - asset_panel.data["model"]["projects"].project_changed, - asset_panel.data["model"]["assets"].selection_changed, - asset_panel.data["model"]["tasks"].task_changed - ]: + for signal in ( + asset_panel.project_bar.project_changed, + asset_panel.assets_widget.selection_changed, + asset_panel.tasks_widget.task_changed + ): signal.connect(self.on_session_changed) # todo: Simplify this callback connection - asset_panel.data["model"]["projects"].project_changed.connect( + asset_panel.project_bar.project_changed.connect( self.on_project_changed ) self.resize(520, 740) def set_page(self, page): - - current = self.pages.currentIndex() + current = self.page_slider.currentIndex() if current == page and self._page == page: return direction = "right" if page > current else "left" self._page = page - self.pages.slide_view(page, direction=direction) + self.page_slider.slide_view(page, direction=direction) def refresh(self): - asset = self.data["pages"]["asset"] - asset.data["model"]["assets"].refresh() + self.asset_panel.assets_widget.refresh() self.refresh_actions() def echo(self, message): - widget = self.data["label"]["message"] - widget.setText(str(message)) - - QtCore.QTimer.singleShot(5000, lambda: widget.setText("")) - + self.message_label.setText(str(message)) + QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText("")) print(message) def on_project_changed(self): - project_name = self.data["pages"]["asset"].data["model"]["projects"].get_current_project() + project_name = self.asset_panel.project_bar.get_current_project() io.Session["AVALON_PROJECT"] = project_name # Update the Action plug-ins available for the current project - self.data["model"]["actions"].model.discover() + self.actions_bar.model.discover() def on_session_changed(self): self.refresh_actions() @@ -385,26 +367,23 @@ class Window(QtWidgets.QDialog): def refresh_actions(self, delay=1): tools_lib.schedule(self.on_refresh_actions, delay) - def on_project_clicked(self, project): - io.Session["AVALON_PROJECT"] = project - asset_panel = self.data["pages"]["asset"] - asset_panel.data["model"]["projects"].refresh() # Refresh projects - asset_panel.set_project(project) + def on_project_clicked(self, project_name): + io.Session["AVALON_PROJECT"] = project_name + # Refresh projects + self.asset_panel.project_bar.refresh() + self.asset_panel.set_project(project_name) self.set_page(1) self.refresh_actions() def on_back_clicked(self): - self.set_page(0) - self.data["pages"]["project"].model.refresh() # Refresh projects + self.project_panel.model.refresh() # Refresh projects self.refresh_actions() def on_refresh_actions(self): session = self.get_current_session() - - actions = self.data["model"]["actions"] - actions.model.set_session(session) - actions.model.refresh() + self.actions_bar.model.set_session(session) + self.actions_bar.model.refresh() def on_action_clicked(self, action): self.echo("Running action: %s" % action.name) @@ -424,69 +403,58 @@ class Window(QtWidgets.QDialog): self.set_session(session) def get_current_session(self): - - index = self._page - if index == 1: + if self._page == 1: # Assets page - return self.data["pages"]["asset"]._get_current_session() + return self.asset_panel.get_current_session() - else: - session = copy.deepcopy(api.Session) + session = copy.deepcopy(api.Session) - # Remove some potential invalid session values - # that we know are not set when not browsing in - # a project. - session.pop("AVALON_PROJECT", None) - session.pop("AVALON_ASSET", None) - session.pop("AVALON_SILO", None) - session.pop("AVALON_TASK", None) + # Remove some potential invalid session values + # that we know are not set when not browsing in + # a project. + session.pop("AVALON_PROJECT", None) + session.pop("AVALON_ASSET", None) + session.pop("AVALON_SILO", None) + session.pop("AVALON_TASK", None) - return session + return session def run_action(self, action, session=None): - if session is None: session = self.get_current_session() # Add to history - history = self.data["model"]["action_history"] - history.add_action(action, session) + self.action_history.add_action(action, session) # Process the Action action().process(session) def set_session(self, session): - - panel = self.data["pages"]["asset"] - - project = session.get("AVALON_PROJECT") + project_name = session.get("AVALON_PROJECT") silo = session.get("AVALON_SILO") - asset = session.get("AVALON_ASSET") - task = session.get("AVALON_TASK") - - if project: + asset_name = session.get("AVALON_ASSET") + task_name = session.get("AVALON_TASK") + if project_name: # Force the "in project" view. self.pages.slide_view(1, direction="right") - - projects = panel.data["model"]["projects"] - index = projects.view.findText(project) + index = self.asset_panel.project_bar.view.findText(project_name) if index >= 0: - projects.view.setCurrentIndex(index) + self.asset_panel.project_bar.view.setCurrentIndex(index) if silo: - panel.data["model"]["assets"].set_silo(silo) + self.asset_panel.assets_widget.set_silo(silo) - if asset: - panel.data["model"]["assets"].select_assets([asset]) + if asset_name: + self.asset_panel.assets_widget.select_assets([asset_name]) - if task: - panel.on_asset_changed() # requires a forced refresh first - panel.data["model"]["tasks"].select_task(task) + if task_name: + # requires a forced refresh first + self.asset_panel.on_asset_changed() + self.asset_panel.assets_widget.select_task(task_name) class Application(QtWidgets.QApplication): - def __init__(self, *args): super(Application, self).__init__(*args) diff --git a/pype/tools/launcher/flickcharm.py b/pype/tools/launcher/flickcharm.py index b4dd69be6c..a5ea5a79d8 100644 --- a/pype/tools/launcher/flickcharm.py +++ b/pype/tools/launcher/flickcharm.py @@ -16,7 +16,6 @@ travelled only very slightly with the cursor. """ import copy -import sys from Qt import QtWidgets, QtCore, QtGui diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 033ac33d66..e7933e9843 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -74,7 +74,7 @@ def get_application_actions(project): return apps -def get_action_icon(self, action, skip_default=False): +def get_action_icon(action): icon_name = action.icon if not icon_name: return None From 62160ab368237ba013cb35052ddb95a242f2aed8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 00:37:09 +0200 Subject: [PATCH 13/31] replaced api.Session with io.Session --- pype/tools/launcher/app.py | 10 +++++----- pype/tools/launcher/models.py | 1 - pype/tools/launcher/widgets.py | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 78c5406fa8..ef00880585 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -2,7 +2,7 @@ import sys import copy from avalon.vendor.Qt import QtWidgets, QtCore, QtGui -from avalon import io, api, style +from avalon import io, style from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget @@ -197,7 +197,7 @@ class AssetsPanel(QtWidgets.QWidget): def on_project_changed(self): project_name = self.project_bar.get_current_project() - api.Session["AVALON_PROJECT"] = project_name + io.Session["AVALON_PROJECT"] = project_name self.assets_widget.refresh() # Force asset change callback to ensure tasks are correctly reset @@ -220,7 +220,7 @@ class AssetsPanel(QtWidgets.QWidget): def get_current_session(self): asset_doc = self.assets_widget.get_active_asset_document() - session = copy.deepcopy(api.Session) + session = copy.deepcopy(io.Session) # Clear some values that we are about to collect if available session.pop("AVALON_SILO", None) @@ -407,7 +407,7 @@ class Window(QtWidgets.QDialog): # Assets page return self.asset_panel.get_current_session() - session = copy.deepcopy(api.Session) + session = copy.deepcopy(io.Session) # Remove some potential invalid session values # that we know are not set when not browsing in @@ -664,7 +664,7 @@ def cli(args): actions.register_environment_actions() io.install() - #api.Session["AVALON_PROJECT"] = project + #io.Session["AVALON_PROJECT"] = project import traceback sys.excepthook = lambda typ, val, tb: traceback.print_last() diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index ce6e0c722e..b78bff950b 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -1,4 +1,3 @@ -import os import copy import logging import collections diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index a264466dbc..9bcd9000d1 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -1,7 +1,7 @@ import copy from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -from avalon import api +from avalon import io from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel @@ -37,7 +37,7 @@ class ProjectBar(QtWidgets.QWidget): self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. - project_name = api.Session.get("AVALON_PROJECT") + project_name = io.Session.get("AVALON_PROJECT") if project_name: self.set_project(project_name) From 2bae09f4e024936017444fe004f32a5fac7f294a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 01:10:25 +0200 Subject: [PATCH 14/31] using custom dbconnector instead of avalon.io --- pype/tools/launcher/app.py | 48 ++++++++++++++++++++-------------- pype/tools/launcher/lib.py | 8 +----- pype/tools/launcher/models.py | 28 +++++++++++++------- pype/tools/launcher/widgets.py | 25 +++++++++++------- 4 files changed, 63 insertions(+), 46 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index ef00880585..6f554110dc 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -2,7 +2,9 @@ import sys import copy from avalon.vendor.Qt import QtWidgets, QtCore, QtGui -from avalon import io, style +from avalon import style + +from pype.modules.ftrack.lib.io_nonsingleton import DbConnector from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget @@ -86,17 +88,19 @@ class ProjectsPanel(QtWidgets.QWidget): project_clicked = QtCore.Signal(str) - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ProjectsPanel, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) - io.install() + self.dbcon = dbcon + self.dbcon.install() + view = IconListView(parent=self) view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(view) - model = ProjectModel() + model = ProjectModel(self.dbcon) model.hide_invisible = True model.refresh() view.setModel(model) @@ -118,31 +122,35 @@ class AssetsPanel(QtWidgets.QWidget): """Assets page""" back_clicked = QtCore.Signal() - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(AssetsPanel, self).__init__(parent=parent) + self.dbcon = dbcon + # project bar - project_bar_widget = QtWidgets.QWidget() + project_bar_widget = QtWidgets.QWidget(self) layout = QtWidgets.QHBoxLayout(project_bar_widget) layout.setSpacing(4) btn_back_icon = qtawesome.icon("fa.angle-left", color="white") - btn_back = QtWidgets.QPushButton() + btn_back = QtWidgets.QPushButton(project_bar_widget) btn_back.setIcon(btn_back_icon) btn_back.setFixedWidth(23) btn_back.setFixedHeight(23) - project_bar = ProjectBar() + project_bar = ProjectBar(self.dbcon, project_bar_widget) layout.addWidget(btn_back) layout.addWidget(project_bar) # assets - assets_proxy_widgets = QtWidgets.QWidget() + assets_proxy_widgets = QtWidgets.QWidget(self) assets_proxy_widgets.setContentsMargins(0, 0, 0, 0) assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets) - assets_widget = AssetWidget() + assets_widget = AssetWidget( + dbcon=self.dbcon, parent=assets_proxy_widgets + ) # Make assets view flickable flick = FlickCharm(parent=self) @@ -153,7 +161,7 @@ class AssetsPanel(QtWidgets.QWidget): assets_layout.addWidget(assets_widget) # tasks - tasks_widget = TasksWidget() + tasks_widget = TasksWidget(self.dbcon, self) body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) body.setSizePolicy( @@ -197,7 +205,7 @@ class AssetsPanel(QtWidgets.QWidget): def on_project_changed(self): project_name = self.project_bar.get_current_project() - io.Session["AVALON_PROJECT"] = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name self.assets_widget.refresh() # Force asset change callback to ensure tasks are correctly reset @@ -220,7 +228,7 @@ class AssetsPanel(QtWidgets.QWidget): def get_current_session(self): asset_doc = self.assets_widget.get_active_asset_document() - session = copy.deepcopy(io.Session) + session = copy.deepcopy(self.dbcon.Session) # Clear some values that we are about to collect if available session.pop("AVALON_SILO", None) @@ -242,6 +250,8 @@ class Window(QtWidgets.QDialog): def __init__(self, parent=None): super(Window, self).__init__(parent) + self.dbcon = DbConnector() + self.setWindowTitle("Launcher") self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) @@ -251,15 +261,15 @@ class Window(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint ) - project_panel = ProjectsPanel() - asset_panel = AssetsPanel() + project_panel = ProjectsPanel(self.dbcon) + asset_panel = AssetsPanel(self.dbcon) page_slider = SlidePageWidget() page_slider.addWidget(project_panel) page_slider.addWidget(asset_panel) # actions - actions_bar = ActionBar() + actions_bar = ActionBar(self.dbcon, self) # statusbar statusbar = QtWidgets.QWidget() @@ -356,7 +366,7 @@ class Window(QtWidgets.QDialog): def on_project_changed(self): project_name = self.asset_panel.project_bar.get_current_project() - io.Session["AVALON_PROJECT"] = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name # Update the Action plug-ins available for the current project self.actions_bar.model.discover() @@ -368,7 +378,7 @@ class Window(QtWidgets.QDialog): tools_lib.schedule(self.on_refresh_actions, delay) def on_project_clicked(self, project_name): - io.Session["AVALON_PROJECT"] = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name # Refresh projects self.asset_panel.project_bar.refresh() self.asset_panel.set_project(project_name) @@ -407,7 +417,7 @@ class Window(QtWidgets.QDialog): # Assets page return self.asset_panel.get_current_session() - session = copy.deepcopy(io.Session) + session = copy.deepcopy(self.dbcon.Session) # Remove some potential invalid session values # that we know are not set when not browsing in diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index e7933e9843..0bbbb55560 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -16,7 +16,7 @@ provides a bridge between the file-based project inventory and configuration. import os from Qt import QtGui -from avalon import io, lib, pipeline +from avalon import lib, pipeline from avalon.vendor import qtawesome from pype.api import resources @@ -24,12 +24,6 @@ ICON_CACHE = {} NOT_FOUND = type("NotFound", (object, ), {}) -def list_project_tasks(): - """List the project task types available in the current project""" - project = io.find_one({"type": "project"}) - return [task["name"] for task in project["config"]["tasks"]] - - def get_application_actions(project): """Define dynamic Application classes for project using `.toml` files diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index b78bff950b..61e240c2eb 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -5,7 +5,7 @@ import collections from . import lib from Qt import QtCore, QtGui from avalon.vendor import qtawesome -from avalon import io, style, api +from avalon import style, api log = logging.getLogger(__name__) @@ -13,8 +13,10 @@ log = logging.getLogger(__name__) class TaskModel(QtGui.QStandardItemModel): """A model listing the tasks combined for a list of assets""" - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(TaskModel, self).__init__(parent=parent) + self.dbcon = dbcon + self._num_assets = 0 self.default_icon = qtawesome.icon( @@ -29,11 +31,11 @@ class TaskModel(QtGui.QStandardItemModel): self._get_task_icons() def _get_task_icons(self): - if io.Session.get("AVALON_PROJECT") is None: + if not self.dbcon.Session.get("AVALON_PROJECT"): return # Get the project configured icons from database - project = io.find_one({"type": "project"}) + project = self.dbcon.find_one({"type": "project"}) for task in project["config"].get("tasks") or []: icon_name = task.get("icon") if icon_name: @@ -52,7 +54,7 @@ class TaskModel(QtGui.QStandardItemModel): if asset_docs is None and asset_ids is not None: # find assets in db by query - asset_docs = list(io.find({ + asset_docs = list(self.dbcon.find({ "type": "asset", "_id": {"$in": asset_ids} })) @@ -108,8 +110,10 @@ class ActionModel(QtGui.QStandardItemModel): ACTION_ROLE = QtCore.Qt.UserRole GROUP_ROLE = QtCore.Qt.UserRole + 1 - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ActionModel, self).__init__(parent=parent) + self.dbcon = dbcon + self._session = {} self._groups = {} self.default_icon = qtawesome.icon("fa.cube", color="white") @@ -120,7 +124,7 @@ class ActionModel(QtGui.QStandardItemModel): def discover(self): """Set up Actions cache. Run this for each new project.""" - if not io.Session.get("AVALON_PROJECT"): + if not self.dbcon.Session.get("AVALON_PROJECT"): self._registered_actions = list() return @@ -128,7 +132,7 @@ class ActionModel(QtGui.QStandardItemModel): actions = api.discover(api.Action) # Get available project actions and the application actions - project_doc = io.find_one({"type": "project"}) + project_doc = self.dbcon.find_one({"type": "project"}) app_actions = lib.get_application_actions(project_doc) actions.extend(app_actions) @@ -233,9 +237,11 @@ class ActionModel(QtGui.QStandardItemModel): class ProjectModel(QtGui.QStandardItemModel): """List of projects""" - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ProjectModel, self).__init__(parent=parent) + self.dbcon = dbcon + self.hide_invisible = False self.project_icon = qtawesome.icon("fa.map", color="white") @@ -251,7 +257,9 @@ class ProjectModel(QtGui.QStandardItemModel): def get_projects(self): project_docs = [] - for project_doc in sorted(io.projects(), key=lambda x: x["name"]): + for project_doc in sorted( + self.dbcon.projects(), key=lambda x: x["name"] + ): if ( self.hide_invisible and not project_doc["data"].get("visible", True) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 9bcd9000d1..4fc7d166cb 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -1,7 +1,6 @@ import copy from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -from avalon import io from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel @@ -11,10 +10,12 @@ from .flickcharm import FlickCharm class ProjectBar(QtWidgets.QWidget): project_changed = QtCore.Signal(int) - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ProjectBar, self).__init__(parent) - self.model = ProjectModel() + self.dbcon = dbcon + + self.model = ProjectModel(self.dbcon) self.model.hide_invisible = True self.project_combobox = QtWidgets.QComboBox() @@ -37,7 +38,7 @@ class ProjectBar(QtWidgets.QWidget): self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. - project_name = io.Session.get("AVALON_PROJECT") + project_name = self.dbcon.Session.get("AVALON_PROJECT") if project_name: self.set_project(project_name) @@ -68,9 +69,11 @@ class ActionBar(QtWidgets.QWidget): action_clicked = QtCore.Signal(object) - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ActionBar, self).__init__(parent) + self.dbcon = dbcon + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(8, 0, 8, 0) @@ -86,7 +89,7 @@ class ActionBar(QtWidgets.QWidget): view.setSpacing(0) view.setWordWrap(True) - model = ActionModel(self) + model = ActionModel(self.dbcon, self) view.setModel(model) delegate = ActionDelegate(model.GROUP_ROLE, self) @@ -140,12 +143,14 @@ class TasksWidget(QtWidgets.QWidget): QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows ) - def __init__(self): - super(TasksWidget, self).__init__() + def __init__(self, dbcon, parent=None): + super(TasksWidget, self).__init__(parent) - view = QtWidgets.QTreeView() + self.dbcon = dbcon + + view = QtWidgets.QTreeView(self) view.setIndentation(0) - model = TaskModel() + model = TaskModel(self.dbcon) view.setModel(model) layout = QtWidgets.QVBoxLayout(self) From 7f7b220d32a689643c341c9a9ea17308e9936c62 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 01:39:18 +0200 Subject: [PATCH 15/31] moved app launching to pype.lib --- pype/lib.py | 176 ++++++++++++++- pype/modules/ftrack/lib/ftrack_app_handler.py | 212 ++---------------- 2 files changed, 188 insertions(+), 200 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index f99cd73e09..ff0c0c0e82 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -7,15 +7,19 @@ import json import collections import logging import itertools +import copy import contextlib import subprocess +import getpass import inspect +import acre +import platform from abc import ABCMeta, abstractmethod from avalon import io, pipeline import six import avalon.api -from .api import config +from .api import config, Anatomy log = logging.getLogger(__name__) @@ -1416,3 +1420,173 @@ def get_latest_version(asset_name, subset_name): assert version, "No version found, this is a bug" return version + + +class ApplicationLaunchFailed(Exception): + pass + + +def launch_application(project_name, asset_name, task_name, app_name): + database = get_avalon_database() + project_document = database[project_name].find_one({"type": "project"}) + asset_document = database[project_name].find_one({ + "type": "asset", + "name": asset_name + }) + + asset_doc_parents = asset_document["data"].get("parents") + hierarchy = "/".join(asset_doc_parents) + + app_def = avalon.lib.get_application(app_name) + app_label = app_def.get("ftrack_label", app_def.get("label", app_name)) + + host_name = app_def["application_dir"] + data = { + "project": { + "name": project_document["name"], + "code": project_document["data"].get("code") + }, + "task": task_name, + "asset": asset_name, + "app": host_name, + "hierarchy": hierarchy + } + + try: + anatomy = Anatomy(project_name) + anatomy_filled = anatomy.format(data) + workdir = os.path.normpath(anatomy_filled["work"]["folder"]) + + except Exception as exc: + raise ApplicationLaunchFailed( + "Error in anatomy.format: {}".format(str(exc)) + ) + + try: + os.makedirs(workdir) + except FileExistsError: + pass + + last_workfile_path = None + extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get(host_name) + if extensions: + # Find last workfile + file_template = anatomy.templates["work"]["file"] + data.update({ + "version": 1, + "user": os.environ.get("PYPE_USERNAME") or getpass.getuser(), + "ext": extensions[0] + }) + + last_workfile_path = avalon.api.last_workfile( + workdir, file_template, data, extensions, True + ) + + # set environments for Avalon + prep_env = copy.deepcopy(os.environ) + prep_env.update({ + "AVALON_PROJECT": project_name, + "AVALON_ASSET": asset_name, + "AVALON_TASK": task_name, + "AVALON_APP": host_name, + "AVALON_APP_NAME": app_name, + "AVALON_HIERARCHY": hierarchy, + "AVALON_WORKDIR": workdir + }) + + start_last_workfile = avalon.api.should_start_last_workfile( + project_name, host_name, task_name + ) + # Store boolean as "0"(False) or "1"(True) + prep_env["AVALON_OPEN_LAST_WORKFILE"] = ( + str(int(bool(start_last_workfile))) + ) + + if ( + start_last_workfile + and last_workfile_path + and os.path.exists(last_workfile_path) + ): + prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path + + prep_env.update(anatomy.roots_obj.root_environments()) + + # collect all the 'environment' attributes from parents + tools_attr = [prep_env["AVALON_APP"], prep_env["AVALON_APP_NAME"]] + tools_env = asset_document["data"].get("tools_env") or [] + tools_attr.extend(tools_env) + + tools_env = acre.get_tools(tools_attr) + env = acre.compute(tools_env) + env = acre.merge(env, current_env=dict(prep_env)) + + # Get path to execute + st_temp_path = os.environ["PYPE_CONFIG"] + os_plat = platform.system().lower() + + # Path to folder with launchers + path = os.path.join(st_temp_path, "launchers", os_plat) + + # Full path to executable launcher + execfile = None + + launch_hook = app_def.get("launch_hook") + if launch_hook: + log.info("launching hook: {}".format(launch_hook)) + ret_val = execute_hook(launch_hook, env=env) + if not ret_val: + raise ApplicationLaunchFailed( + "Hook didn't finish successfully {}".format(app_label) + ) + + if sys.platform == "win32": + for ext in os.environ["PATHEXT"].split(os.pathsep): + fpath = os.path.join(path.strip('"'), app_def["executable"] + ext) + if os.path.isfile(fpath) and os.access(fpath, os.X_OK): + execfile = fpath + break + + # Run SW if was found executable + if execfile is None: + raise ApplicationLaunchFailed( + "We didn't find launcher for {}".format(app_label) + ) + + popen = avalon.lib.launch( + executable=execfile, args=[], environment=env + ) + + elif ( + sys.platform.startswith("linux") + or sys.platform.startswith("darwin") + ): + execfile = os.path.join(path.strip('"'), app_def["executable"]) + # Run SW if was found executable + if execfile is None: + raise ApplicationLaunchFailed( + "We didn't find launcher for {}".format(app_label) + ) + + if not os.path.isfile(execfile): + raise ApplicationLaunchFailed( + "Launcher doesn't exist - {}".format(execfile) + ) + + try: + fp = open(execfile) + except PermissionError as perm_exc: + raise ApplicationLaunchFailed( + "Access denied on launcher {} - {}".format(execfile, perm_exc) + ) + + fp.close() + # check executable permission + if not os.access(execfile, os.X_OK): + raise ApplicationLaunchFailed( + "No executable permission - {}".format(execfile) + ) + + popen = avalon.lib.launch( # noqa: F841 + "/usr/bin/env", args=["bash", execfile], environment=env + ) + return popen diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 22fd6eeaab..4847464973 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -1,16 +1,6 @@ -import os -import sys -import copy -import platform -import avalon.lib -import acre -import getpass from pype import lib as pypelib -from pype.api import config, Anatomy +from pype.api import config from .ftrack_action_handler import BaseAction -from avalon.api import ( - last_workfile, HOST_WORKFILE_EXTENSIONS, should_start_last_workfile -) class AppAction(BaseAction): @@ -156,43 +146,21 @@ class AppAction(BaseAction): entity = entities[0] task_name = entity["name"] - - project_name = entity["project"]["full_name"] - - database = pypelib.get_avalon_database() - asset_name = entity["parent"]["name"] - asset_document = database[project_name].find_one({ - "type": "asset", - "name": asset_name - }) - - hierarchy = "" - asset_doc_parents = asset_document["data"].get("parents") - if asset_doc_parents: - hierarchy = os.path.join(*asset_doc_parents) - - application = avalon.lib.get_application(self.identifier) - host_name = application["application_dir"] - data = { - "project": { - "name": entity["project"]["full_name"], - "code": entity["project"]["name"] - }, - "task": task_name, - "asset": asset_name, - "app": host_name, - "hierarchy": hierarchy - } - + project_name = entity["project"]["full_name"] try: - anatomy = Anatomy(project_name) - anatomy_filled = anatomy.format(data) - workdir = os.path.normpath(anatomy_filled["work"]["folder"]) + pypelib.launch_application(project_name, asset_name, task_name) - except Exception as exc: - msg = "Error in anatomy.format: {}".format( - str(exc) + except pypelib.ApplicationLaunchFailed as exc: + self.log.error(str(exc)) + return { + "success": False, + "message": str(exc) + } + + except Exception: + msg = "Unexpected failure of application launch {}".format( + self.label ) self.log.error(msg, exc_info=True) return { @@ -200,160 +168,6 @@ class AppAction(BaseAction): "message": msg } - try: - os.makedirs(workdir) - except FileExistsError: - pass - - last_workfile_path = None - extensions = HOST_WORKFILE_EXTENSIONS.get(host_name) - if extensions: - # Find last workfile - file_template = anatomy.templates["work"]["file"] - data.update({ - "version": 1, - "user": getpass.getuser(), - "ext": extensions[0] - }) - - last_workfile_path = last_workfile( - workdir, file_template, data, extensions, True - ) - - # set environments for Avalon - prep_env = copy.deepcopy(os.environ) - prep_env.update({ - "AVALON_PROJECT": project_name, - "AVALON_ASSET": asset_name, - "AVALON_TASK": task_name, - "AVALON_APP": host_name, - "AVALON_APP_NAME": self.identifier, - "AVALON_HIERARCHY": hierarchy, - "AVALON_WORKDIR": workdir - }) - - start_last_workfile = should_start_last_workfile( - project_name, host_name, task_name - ) - # Store boolean as "0"(False) or "1"(True) - prep_env["AVALON_OPEN_LAST_WORKFILE"] = ( - str(int(bool(start_last_workfile))) - ) - - if ( - start_last_workfile - and last_workfile_path - and os.path.exists(last_workfile_path) - ): - prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path - - prep_env.update(anatomy.roots_obj.root_environments()) - - # collect all parents from the task - parents = [] - for item in entity['link']: - parents.append(session.get(item['type'], item['id'])) - - # collect all the 'environment' attributes from parents - tools_attr = [prep_env["AVALON_APP"], prep_env["AVALON_APP_NAME"]] - tools_env = asset_document["data"].get("tools_env") or [] - tools_attr.extend(tools_env) - - tools_env = acre.get_tools(tools_attr) - env = acre.compute(tools_env) - env = acre.merge(env, current_env=dict(prep_env)) - - # Get path to execute - st_temp_path = os.environ["PYPE_CONFIG"] - os_plat = platform.system().lower() - - # Path to folder with launchers - path = os.path.join(st_temp_path, "launchers", os_plat) - - # Full path to executable launcher - execfile = None - - if application.get("launch_hook"): - hook = application.get("launch_hook") - self.log.info("launching hook: {}".format(hook)) - ret_val = pypelib.execute_hook( - application.get("launch_hook"), env=env) - if not ret_val: - return { - 'success': False, - 'message': "Hook didn't finish successfully {0}" - .format(self.label) - } - - if sys.platform == "win32": - for ext in os.environ["PATHEXT"].split(os.pathsep): - fpath = os.path.join(path.strip('"'), self.executable + ext) - if os.path.isfile(fpath) and os.access(fpath, os.X_OK): - execfile = fpath - break - - # Run SW if was found executable - if execfile is None: - return { - "success": False, - "message": "We didn't find launcher for {0}".format( - self.label - ) - } - - popen = avalon.lib.launch( - executable=execfile, args=[], environment=env - ) - - elif (sys.platform.startswith("linux") - or sys.platform.startswith("darwin")): - execfile = os.path.join(path.strip('"'), self.executable) - if not os.path.isfile(execfile): - msg = "Launcher doesn't exist - {}".format(execfile) - - self.log.error(msg) - return { - "success": False, - "message": msg - } - - try: - fp = open(execfile) - except PermissionError as perm_exc: - msg = "Access denied on launcher {} - {}".format( - execfile, perm_exc - ) - - self.log.exception(msg, exc_info=True) - return { - "success": False, - "message": msg - } - - fp.close() - # check executable permission - if not os.access(execfile, os.X_OK): - msg = "No executable permission - {}".format(execfile) - - self.log.error(msg) - return { - "success": False, - "message": msg - } - - # Run SW if was found executable - if execfile is None: - return { - "success": False, - "message": "We didn't found launcher for {0}".format( - self.label - ) - } - - popen = avalon.lib.launch( # noqa: F841 - "/usr/bin/env", args=["bash", execfile], environment=env - ) - # Change status of task to In progress presets = config.get_presets()["ftrack"]["ftrack_config"] From 83ac4df7b68db7f4983db6f37560e34c5b494fa9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 01:48:30 +0200 Subject: [PATCH 16/31] added application action to pype.lib --- pype/lib.py | 32 ++++++++++++++++++++++++++++++++ pype/tools/launcher/lib.py | 5 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index ff0c0c0e82..46dd2b781d 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1590,3 +1590,35 @@ def launch_application(project_name, asset_name, task_name, app_name): "/usr/bin/env", args=["bash", execfile], environment=env ) return popen + + +class ApplicationAction(avalon.api.Action): + """Default application launcher + + This is a convenience application Action that when "config" refers to a + parsed application `.toml` this can launch the application. + + """ + + config = None + required_session_keys = ( + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK" + ) + + def is_compatible(self, session): + for key in self.required_session_keys: + if key not in session: + return False + return True + + def process(self, session, **kwargs): + """Process the full Application action""" + + project_name = session["AVALON_PROJECT"] + asset_name = session["AVALON_ASSET"] + task_name = session["AVALON_TASK"] + return launch_application( + project_name, asset_name, task_name, self.name + ) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 0bbbb55560..027ae96e84 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -16,9 +16,10 @@ provides a bridge between the file-based project inventory and configuration. import os from Qt import QtGui -from avalon import lib, pipeline +from avalon import lib from avalon.vendor import qtawesome from pype.api import resources +from pype.lib import ApplicationAction ICON_CACHE = {} NOT_FOUND = type("NotFound", (object, ), {}) @@ -52,7 +53,7 @@ def get_application_actions(project): action = type( "app_{}".format(app_name), - (pipeline.Application,), + (ApplicationAction,), { "name": app_name, "label": label, From 4f2ffa6022af28355e49bc13c5713a2f8490b658 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:21:47 +0200 Subject: [PATCH 17/31] final launcher touches --- pype/tools/launcher/__init__.py | 11 +- pype/tools/launcher/__main__.py | 5 - pype/tools/launcher/actions.py | 12 +- pype/tools/launcher/app.py | 242 ++------------------------------ pype/tools/launcher/lib.py | 1 + 5 files changed, 24 insertions(+), 247 deletions(-) delete mode 100644 pype/tools/launcher/__main__.py diff --git a/pype/tools/launcher/__init__.py b/pype/tools/launcher/__init__.py index 3b88ebe984..8f7bf5a769 100644 --- a/pype/tools/launcher/__init__.py +++ b/pype/tools/launcher/__init__.py @@ -1,10 +1,7 @@ - -from .app import ( - show, - cli -) +from .app import LauncherWindow +from . import actions __all__ = [ - "show", - "cli", + "LauncherWindow", + "actions" ] diff --git a/pype/tools/launcher/__main__.py b/pype/tools/launcher/__main__.py deleted file mode 100644 index 50642c46cd..0000000000 --- a/pype/tools/launcher/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from app import cli - -if __name__ == '__main__': - import sys - sys.exit(cli(sys.argv[1:])) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index 44ba9a3a60..80e6f71ae7 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -8,7 +8,6 @@ class ProjectManagerAction(api.Action): name = "projectmanager" label = "Project Manager" icon = "gear" - group = "Test" order = 999 # at the end def is_compatible(self, session): @@ -28,8 +27,7 @@ class LoaderAction(api.Action): name = "loader" label = "Loader" icon = "cloud-download" - order = 998 # at the end - group = "Test" + order = 998 def is_compatible(self, session): return "AVALON_PROJECT" in session @@ -38,7 +36,7 @@ class LoaderAction(api.Action): return lib.launch( executable="python", args=[ - "-u", "-m", "avalon.tools.cbloader", session['AVALON_PROJECT'] + "-u", "-m", "avalon.tools.loader", session['AVALON_PROJECT'] ] ) @@ -72,8 +70,10 @@ def register_config_actions(): module_name = os.environ["AVALON_CONFIG"] config = importlib.import_module(module_name) if not hasattr(config, "register_launcher_actions"): - print("Current configuration `%s` has no 'register_launcher_actions'" - % config.__name__) + print( + "Current configuration `%s` has no 'register_launcher_actions'" + % config.__name__ + ) return config.register_launcher_actions() diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 6f554110dc..b6a7d4dab2 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -1,10 +1,10 @@ -import sys import copy -from avalon.vendor.Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore, QtGui from avalon import style from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from pype.api import resources from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget @@ -16,10 +16,6 @@ from .widgets import ( from .flickcharm import FlickCharm -module = sys.modules[__name__] -module.window = None - - class IconListView(QtWidgets.QListView): """Styled ListView that allows to toggle between icon and list mode. @@ -244,17 +240,21 @@ class AssetsPanel(QtWidgets.QWidget): return session -class Window(QtWidgets.QDialog): +class LauncherWindow(QtWidgets.QDialog): """Launcher interface""" def __init__(self, parent=None): - super(Window, self).__init__(parent) + super(LauncherWindow, self).__init__(parent) self.dbcon = DbConnector() self.setWindowTitle("Launcher") self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + self.setStyleSheet(style.load_stylesheet()) # Allow minimize self.setWindowFlags( @@ -287,8 +287,10 @@ class Window(QtWidgets.QDialog): # Vertically split Pages and Actions body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) - body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) body.setOrientation(QtCore.Qt.Vertical) body.addWidget(page_slider) body.addWidget(actions_bar) @@ -462,221 +464,3 @@ class Window(QtWidgets.QDialog): # requires a forced refresh first self.asset_panel.on_asset_changed() self.asset_panel.assets_widget.select_task(task_name) - - -class Application(QtWidgets.QApplication): - def __init__(self, *args): - super(Application, self).__init__(*args) - - # Set app icon - icon_path = tools_lib.resource("icons", "png", "avalon-logo-16.png") - icon = QtGui.QIcon(icon_path) - - self.setWindowIcon(icon) - - # Toggles - self.toggles = {"autoHide": False} - - # Timers - keep_visible = QtCore.QTimer(self) - keep_visible.setInterval(100) - keep_visible.setSingleShot(True) - - timers = {"keepVisible": keep_visible} - - tray = QtWidgets.QSystemTrayIcon(icon) - tray.setToolTip("Avalon Launcher") - - # Signals - tray.activated.connect(self.on_tray_activated) - self.aboutToQuit.connect(self.on_quit) - - menu = self.build_menu() - tray.setContextMenu(menu) - tray.show() - - tray.showMessage("Avalon", "Launcher started.") - - # Don't close the app when we close the log window. - # self.setQuitOnLastWindowClosed(False) - - self.focusChanged.connect(self.on_focus_changed) - - window = Window() - window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) - - self.timers = timers - self._tray = tray - self._window = window - - # geometry = self.calculate_window_geometry(window) - # window.setGeometry(geometry) - - def show(self): - """Show the primary GUI - - This also activates the window and deals with platform-differences. - - """ - - self._window.show() - self._window.raise_() - self._window.activateWindow() - - self.timers["keepVisible"].start() - - def on_tray_activated(self, reason): - if self._window.isVisible(): - self._window.hide() - - elif reason == QtWidgets.QSystemTrayIcon.Trigger: - self.show() - - def on_focus_changed(self, old, new): - """Respond to window losing focus""" - window = new - keep_visible = self.timers["keepVisible"].isActive() - self._window.hide() if (self.toggles["autoHide"] and - not window and - not keep_visible) else None - - def on_autohide_changed(self, auto_hide): - """Respond to changes to auto-hide - - Auto-hide is changed in the UI and determines whether or not - the UI hides upon losing focus. - - """ - - self.toggles["autoHide"] = auto_hide - self.echo("Hiding when losing focus" if auto_hide else "Stays visible") - - def on_quit(self): - """Respond to the application quitting""" - self._tray.hide() - - def build_menu(self): - """Build the right-mouse context menu for the tray icon""" - menu = QtWidgets.QMenu() - - icon = qtawesome.icon("fa.eye", color=style.colors.default) - open = QtWidgets.QAction(icon, "Open", self) - open.triggered.connect(self.show) - - def toggle(): - self.on_autohide_changed(not self.toggles['autoHide']) - - keep_open = QtWidgets.QAction("Keep open", self) - keep_open.setCheckable(True) - keep_open.setChecked(not self.toggles['autoHide']) - keep_open.triggered.connect(toggle) - - quit = QtWidgets.QAction("Quit", self) - quit.triggered.connect(self.quit) - - menu.setStyleSheet(""" - QMenu { - padding: 0px; - margin: 0px; - } - """) - - for action in [open, keep_open, quit]: - menu.addAction(action) - - return menu - - def calculate_window_geometry(self, window): - """Respond to status changes - - On creation, align window with where the tray icon is - located. For example, if the tray icon is in the upper - right corner of the screen, then this is where the - window is supposed to appear. - - Arguments: - status (int): Provided by Qt, the status flag of - loading the input file. - - """ - - tray_x = self._tray.geometry().x() - tray_y = self._tray.geometry().y() - - width = window.width() - width = max(width, window.minimumWidth()) - - height = window.height() - height = max(height, window.sizeHint().height()) - - desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() - screen_geometry = window.geometry() - - screen_width = screen_geometry.width() - screen_height = screen_geometry.height() - - # Calculate width and height of system tray - systray_width = screen_geometry.width() - desktop_geometry.width() - systray_height = screen_geometry.height() - desktop_geometry.height() - - padding = 10 - - x = screen_width - width - y = screen_height - height - - if tray_x < (screen_width / 2): - x = 0 + systray_width + padding - else: - x -= systray_width + padding - - if tray_y < (screen_height / 2): - y = 0 + systray_height + padding - else: - y -= systray_height + padding - - return QtCore.QRect(x, y, width, height) - - -def show(root=None, debug=False, parent=None): - """Display Loader GUI - - Arguments: - debug (bool, optional): Run loader in debug-mode, - defaults to False - parent (QtCore.QObject, optional): When provided parent the interface - to this QObject. - - """ - - app = Application(sys.argv) - app.setStyleSheet(style.load_stylesheet()) - - # Show the window on launch - app.show() - - app.exec_() - - -def cli(args): - import argparse - parser = argparse.ArgumentParser() - #parser.add_argument("project") - - args = parser.parse_args(args) - #project = args.project - - import launcher.actions as actions - print("Registering default actions..") - actions.register_default_actions() - print("Registering config actions..") - actions.register_config_actions() - print("Registering environment actions..") - actions.register_environment_actions() - io.install() - - #io.Session["AVALON_PROJECT"] = project - - import traceback - sys.excepthook = lambda typ, val, tb: traceback.print_last() - - show() diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 027ae96e84..d307e146d5 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -95,6 +95,7 @@ def get_action_icon(action): ) except Exception: + ICON_CACHE[icon_name] = NOT_FOUND print("Can't load icon \"{}\"".format(icon_name)) return icon From eb16141a0e3c07583dd4efd4fc0c0d113548e725 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:22:01 +0200 Subject: [PATCH 18/31] modified avalon_apps module --- pype/modules/avalon_apps/avalon_app.py | 33 ++++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index d103a84d90..393e1fe755 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -1,10 +1,7 @@ -import os -import argparse -from Qt import QtGui, QtWidgets +from Qt import QtWidgets from avalon.tools import libraryloader from pype.api import Logger -from avalon import io -from launcher import launcher_widget, lib as launcher_lib +from pype.tools.launcher import LauncherWindow, actions class AvalonApps: @@ -12,7 +9,12 @@ class AvalonApps: self.log = Logger().get_logger(__name__) self.main_parent = main_parent self.parent = parent - self.app_launcher = None + + self.app_launcher = LauncherWindow() + + # actions.register_default_actions() + actions.register_config_actions() + actions.register_environment_actions() def process_modules(self, modules): if "RestApiServer" in modules: @@ -32,23 +34,18 @@ class AvalonApps: self.log.warning('Parent menu is not set') return - icon = QtGui.QIcon(launcher_lib.resource("icon", "main.png")) - aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) - aLibraryLoader = QtWidgets.QAction("Library", parent_menu) + action_launcher = QtWidgets.QAction("Launcher", parent_menu) + action_library_loader = QtWidgets.QAction("Library", parent_menu) - aShowLauncher.triggered.connect(self.show_launcher) - aLibraryLoader.triggered.connect(self.show_library_loader) + action_launcher.triggered.connect(self.show_launcher) + action_library_loader.triggered.connect(self.show_library_loader) - parent_menu.addAction(aShowLauncher) - parent_menu.addAction(aLibraryLoader) + parent_menu.addAction(action_launcher) + parent_menu.addAction(action_library_loader) def show_launcher(self): # if app_launcher don't exist create it/otherwise only show main window - if self.app_launcher is None: - io.install() - APP_PATH = launcher_lib.resource("qml", "main.qml") - self.app_launcher = launcher_widget.Launcher(APP_PATH) - self.app_launcher.window.show() + self.app_launcher.show() def show_library_loader(self): libraryloader.show( From 03c3b4213ca0f155d4cd32589a1ed4687feada99 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:30:28 +0200 Subject: [PATCH 19/31] modified library loader action label --- pype/modules/avalon_apps/avalon_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index 393e1fe755..34fbc5c5ae 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -35,7 +35,9 @@ class AvalonApps: return action_launcher = QtWidgets.QAction("Launcher", parent_menu) - action_library_loader = QtWidgets.QAction("Library", parent_menu) + action_library_loader = QtWidgets.QAction( + "Library loader", parent_menu + ) action_launcher.triggered.connect(self.show_launcher) action_library_loader.triggered.connect(self.show_library_loader) From 68df56b31b79535b9f51e05aa776dbeb9478cd01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:33:32 +0200 Subject: [PATCH 20/31] do not use group delegate yet --- pype/tools/launcher/widgets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 4fc7d166cb..391f9f90f7 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -92,8 +92,9 @@ class ActionBar(QtWidgets.QWidget): model = ActionModel(self.dbcon, self) view.setModel(model) - delegate = ActionDelegate(model.GROUP_ROLE, self) - view.setItemDelegate(delegate) + # TODO better group delegate + # delegate = ActionDelegate(model.GROUP_ROLE, self) + # view.setItemDelegate(delegate) layout.addWidget(view) From 7c6849ce6a649aec763c63a89c05f3ae8218dc00 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:39:55 +0200 Subject: [PATCH 21/31] small tweaks --- pype/tools/launcher/app.py | 1 + pype/tools/launcher/delegates.py | 4 ++-- pype/tools/launcher/widgets.py | 2 +- pype/tools/tray/__main__.py | 10 +++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index b6a7d4dab2..df2a47eed8 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -16,6 +16,7 @@ from .widgets import ( from .flickcharm import FlickCharm + class IconListView(QtWidgets.QListView): """Styled ListView that allows to toggle between icon and list mode. diff --git a/pype/tools/launcher/delegates.py b/pype/tools/launcher/delegates.py index 750301cec4..8e1ec2004e 100644 --- a/pype/tools/launcher/delegates.py +++ b/pype/tools/launcher/delegates.py @@ -3,8 +3,8 @@ from Qt import QtCore, QtWidgets, QtGui class ActionDelegate(QtWidgets.QStyledItemDelegate): extender_lines = 2 - extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) - extender_fg = QtGui.QColor(255, 255, 255)#, 160) + extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100, 160)) + extender_fg = QtGui.QColor(255, 255, 255, 160) def __init__(self, group_role, *args, **kwargs): super(ActionDelegate, self).__init__(*args, **kwargs) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 391f9f90f7..21546e286e 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -2,7 +2,7 @@ import copy from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -from .delegates import ActionDelegate +# from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm diff --git a/pype/tools/tray/__main__.py b/pype/tools/tray/__main__.py index d0006c0afe..94d5461dc4 100644 --- a/pype/tools/tray/__main__.py +++ b/pype/tools/tray/__main__.py @@ -1,4 +1,12 @@ +import os import sys import pype_tray -sys.exit(pype_tray.PypeTrayApplication().exec_()) +app = pype_tray.PypeTrayApplication() +if os.name == "nt": + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + u"pype_tray" + ) + +sys.exit(app.exec_()) From 26142007452d7c0d213797af729587da39ef1354 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 03:01:32 +0200 Subject: [PATCH 22/31] app.py renamed to window.py --- pype/tools/launcher/__init__.py | 2 +- pype/tools/launcher/{app.py => window.py} | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) rename pype/tools/launcher/{app.py => window.py} (97%) diff --git a/pype/tools/launcher/__init__.py b/pype/tools/launcher/__init__.py index 8f7bf5a769..109d642e86 100644 --- a/pype/tools/launcher/__init__.py +++ b/pype/tools/launcher/__init__.py @@ -1,4 +1,4 @@ -from .app import LauncherWindow +from .window import LauncherWindow from . import actions __all__ = [ diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/window.py similarity index 97% rename from pype/tools/launcher/app.py rename to pype/tools/launcher/window.py index df2a47eed8..b53b5b415c 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/window.py @@ -1,4 +1,5 @@ import copy +import logging from Qt import QtWidgets, QtCore, QtGui from avalon import style @@ -247,6 +248,9 @@ class LauncherWindow(QtWidgets.QDialog): def __init__(self, parent=None): super(LauncherWindow, self).__init__(parent) + self.log = logging.getLogger( + ".".join([__name__, self.__class__.__name__]) + ) self.dbcon = DbConnector() self.setWindowTitle("Launcher") @@ -365,7 +369,7 @@ class LauncherWindow(QtWidgets.QDialog): def echo(self, message): self.message_label.setText(str(message)) QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText("")) - print(message) + self.log.debug(message) def on_project_changed(self): project_name = self.asset_panel.project_bar.get_current_project() @@ -399,7 +403,7 @@ class LauncherWindow(QtWidgets.QDialog): self.actions_bar.model.refresh() def on_action_clicked(self, action): - self.echo("Running action: %s" % action.name) + self.echo("Running action: {}".format(action.name)) self.run_action(action) def on_history_action(self, history_data): @@ -440,7 +444,11 @@ class LauncherWindow(QtWidgets.QDialog): self.action_history.add_action(action, session) # Process the Action - action().process(session) + try: + action().process(session) + except Exception as exc: + self.log.warning("Action launch failed.", exc_info=True) + self.echo("Failed: {}".format(str(exc))) def set_session(self, session): project_name = session.get("AVALON_PROJECT") From f011223f22ec987ab84db6a582a21775989c598e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 03:05:14 +0200 Subject: [PATCH 23/31] hound fix --- pype/tools/launcher/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 61e240c2eb..fee09e4f4b 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -76,9 +76,9 @@ class TaskModel(QtGui.QStandardItemModel): if not asset_docs: return - task_names = collections.Counter() + task_names = set() for asset_doc in asset_docs: - asset_tasks = asset_doc.get("data", {}).get("tasks", []) + asset_tasks = asset_doc.get("data", {}).get("tasks") or set() task_names.update(asset_tasks) self.beginResetModel() @@ -89,7 +89,7 @@ class TaskModel(QtGui.QStandardItemModel): self.appendRow(item) else: - for task_name, count in sorted(task_names.items()): + for task_name in sorted(task_names): icon = self._icons.get(task_name, self.default_icon) item = QtGui.QStandardItem(icon, task_name) self.appendRow(item) From 3a096e5b638ccf47199eb30d7c390f6a5c06d432 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 10:10:18 +0200 Subject: [PATCH 24/31] fixed actions history --- pype/tools/launcher/widgets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 21546e286e..c3a908c9dd 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome # from .delegates import ActionDelegate +from . import lib from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm @@ -264,9 +265,7 @@ class ActionHistory(QtWidgets.QPushButton): m = "{{action:{0}}} | {{breadcrumb}}".format(largest_action_label) label = m.format(action=action.label, breadcrumb=breadcrumb) - icon_name = action.icon - color = action.color or "white" - icon = qtawesome.icon("fa.%s" % icon_name, color=color) + icon = lib.get_action_icon(action) item = QtWidgets.QListWidgetItem(icon, label) item.setData(action_session_role, (action, session)) From 00523f0c500412721c50907ef35deacc3c7c7569 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:02:23 +0200 Subject: [PATCH 25/31] added label_variant to actions --- pype/tools/launcher/lib.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index d307e146d5..25270fcbfe 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -48,15 +48,16 @@ def get_application_actions(project): icon = app_definition.get("icon", app.get("icon", "folder-o")) color = app_definition.get("color", app.get("color", None)) order = app_definition.get("order", app.get("order", 0)) - label = app.get("label") or app_definition.get("label") or app["name"] - group = app.get("group") or app_definition.get("group") - + label = app_definition.get("label") or app.get("label") or app_name + label_variant = app_definition.get("label_variant") + group = app_definition.get("group") or app.get("group") action = type( "app_{}".format(app_name), (ApplicationAction,), { "name": app_name, "label": label, + "label_variant": label_variant, "group": group, "icon": icon, "color": color, From 15f97fbad1ef10657be83f00d2efea7996bce567 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:02:56 +0200 Subject: [PATCH 26/31] variantss are showing in proper way --- pype/tools/launcher/lib.py | 11 +++++++ pype/tools/launcher/models.py | 54 +++++++++++++++++++++++++++------- pype/tools/launcher/widgets.py | 51 ++++++++++++++++++++++++++++---- 3 files changed, 100 insertions(+), 16 deletions(-) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 25270fcbfe..a6d6ff6865 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -100,3 +100,14 @@ def get_action_icon(action): print("Can't load icon \"{}\"".format(icon_name)) return icon + + +def get_action_label(action): + label = getattr(action, "label", None) + if not label: + return action.name + + label_variant = getattr(action, "label_variant", None) + if not label_variant: + return label + return " ".join([label, label_variant]) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index fee09e4f4b..f76e26afde 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -155,25 +155,57 @@ class ActionModel(QtGui.QStandardItemModel): self.beginResetModel() single_actions = [] + varianted_actions = collections.defaultdict(list) grouped_actions = collections.defaultdict(list) for action in actions: + # Groups group_name = getattr(action, "group", None) - if not group_name: - single_actions.append(action) - else: + + # Lable variants + label = getattr(action, "label", None) + label_variant = getattr(action, "label_variant", None) + if label_variant and not label: + print(( + "Invalid action \"{}\" has set `label_variant` to \"{}\"" + ", but doesn't have set `label` attribute" + ).format(action.name, label_variant)) + action.label_variant = None + label_variant = None + + if group_name: grouped_actions[group_name].append(action) - for group_name, actions in tuple(grouped_actions.items()): - if len(actions) == 1: - grouped_actions.pop(group_name) - single_actions.append(actions[0]) + elif label_variant: + varianted_actions[label].append(action) + else: + single_actions.append(action) items_by_order = collections.defaultdict(list) + for label, actions in tuple(varianted_actions.items()): + if len(actions) == 1: + varianted_actions.pop(label) + single_actions.append(actions[0]) + continue + + icon = None + order = None + for action in actions: + if icon is None: + _icon = lib.get_action_icon(action) + if _icon: + icon = _icon + + if order is None or action.order < order: + order = action.order + + item = QtGui.QStandardItem(icon, action.label) + item.setData(actions, self.ACTION_ROLE) + item.setData(True, self.GROUP_ROLE) + items_by_order[order].append(item) + for action in single_actions: icon = self.get_icon(action) - item = QtGui.QStandardItem( - icon, str(action.label or action.name) - ) + item = QtGui.QStandardItem(icon, lib.get_action_label(action)) item.setData(action, self.ACTION_ROLE) items_by_order[action.order].append(item) @@ -185,7 +217,7 @@ class ActionModel(QtGui.QStandardItemModel): order = action.order if icon is None: - _icon = self.get_icon(action) + _icon = lib.get_action_icon(action) if _icon: icon = _icon diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index c3a908c9dd..774c3de5ee 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -1,4 +1,5 @@ import copy +import collections from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome @@ -123,13 +124,53 @@ class ActionBar(QtWidgets.QWidget): self.action_clicked.emit(action) return - menu = QtWidgets.QMenu(self) actions = index.data(self.model.ACTION_ROLE) - actions_mapping = {} + by_variant_label = collections.defaultdict(list) + orders = [] for action in actions: - menu_action = QtWidgets.QAction(action.label or action.name) - menu.addAction(menu_action) - actions_mapping[menu_action] = action + # Lable variants + label = getattr(action, "label", None) + label_variant = getattr(action, "label_variant", None) + if label_variant and not label: + label_variant = None + + if not label_variant: + orders.append(action) + continue + + if label not in orders: + orders.append(label) + by_variant_label[label].append(action) + + menu = QtWidgets.QMenu(self) + actions_mapping = {} + + for action_item in orders: + actions = by_variant_label.get(action_item) + if not actions: + action = action_item + elif len(actions) == 1: + action = actions[0] + else: + action = None + + if action: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + continue + + sub_menu = QtWidgets.QMenu(label, menu) + for action in actions: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + sub_menu.addAction(menu_action) + actions_mapping[menu_action] = action + + menu.addMenu(sub_menu) result = menu.exec_(QtGui.QCursor.pos()) if result: From 8b4f209da42fe4e20d8c5cccfec4b4ba5c09f7bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:03:19 +0200 Subject: [PATCH 27/31] added group and variant to ApplicationAction --- pype/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 46dd2b781d..00bd72a164 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1601,6 +1601,8 @@ class ApplicationAction(avalon.api.Action): """ config = None + group = None + variant = None required_session_keys = ( "AVALON_PROJECT", "AVALON_ASSET", From 0bda8c0d2faf86d0b5c47abb1f407491979b312e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:03:40 +0200 Subject: [PATCH 28/31] use non perfect group delegate to make clear what's grouped --- pype/tools/launcher/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 774c3de5ee..82435e8681 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -3,7 +3,7 @@ import collections from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -# from .delegates import ActionDelegate +from .delegates import ActionDelegate from . import lib from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm @@ -95,8 +95,8 @@ class ActionBar(QtWidgets.QWidget): view.setModel(model) # TODO better group delegate - # delegate = ActionDelegate(model.GROUP_ROLE, self) - # view.setItemDelegate(delegate) + delegate = ActionDelegate(model.GROUP_ROLE, self) + view.setItemDelegate(delegate) layout.addWidget(view) From 33ea814c890a9c3c71d1f18945786f3ab7856e4e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:23:59 +0200 Subject: [PATCH 29/31] variants are working now --- pype/tools/launcher/delegates.py | 10 ++-- pype/tools/launcher/models.py | 3 +- pype/tools/launcher/widgets.py | 87 ++++++++++++++++++-------------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/pype/tools/launcher/delegates.py b/pype/tools/launcher/delegates.py index 8e1ec2004e..95ccde6445 100644 --- a/pype/tools/launcher/delegates.py +++ b/pype/tools/launcher/delegates.py @@ -6,13 +6,17 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100, 160)) extender_fg = QtGui.QColor(255, 255, 255, 160) - def __init__(self, group_role, *args, **kwargs): + def __init__(self, group_roles, *args, **kwargs): super(ActionDelegate, self).__init__(*args, **kwargs) - self.group_role = group_role + self.group_roles = group_roles def paint(self, painter, option, index): super(ActionDelegate, self).paint(painter, option, index) - is_group = index.data(self.group_role) + is_group = False + for group_role in self.group_roles: + is_group = index.data(group_role) + if is_group: + break if not is_group: return diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index f76e26afde..3fb201702e 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -109,6 +109,7 @@ class TaskModel(QtGui.QStandardItemModel): class ActionModel(QtGui.QStandardItemModel): ACTION_ROLE = QtCore.Qt.UserRole GROUP_ROLE = QtCore.Qt.UserRole + 1 + VARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2 def __init__(self, dbcon, parent=None): super(ActionModel, self).__init__(parent=parent) @@ -200,7 +201,7 @@ class ActionModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(icon, action.label) item.setData(actions, self.ACTION_ROLE) - item.setData(True, self.GROUP_ROLE) + item.setData(True, self.VARIANT_GROUP_ROLE) items_by_order[order].append(item) for action in single_actions: diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 82435e8681..7ab0a3f8ea 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -95,7 +95,10 @@ class ActionBar(QtWidgets.QWidget): view.setModel(model) # TODO better group delegate - delegate = ActionDelegate(model.GROUP_ROLE, self) + delegate = ActionDelegate( + [model.GROUP_ROLE, model.VARIANT_GROUP_ROLE], + self + ) view.setItemDelegate(delegate) layout.addWidget(view) @@ -119,58 +122,68 @@ class ActionBar(QtWidgets.QWidget): return is_group = index.data(self.model.GROUP_ROLE) - if not is_group: + is_variant_group = index.data(self.model.VARIANT_GROUP_ROLE) + if not is_group and not is_variant_group: action = index.data(self.model.ACTION_ROLE) self.action_clicked.emit(action) return actions = index.data(self.model.ACTION_ROLE) - by_variant_label = collections.defaultdict(list) - orders = [] - for action in actions: - # Lable variants - label = getattr(action, "label", None) - label_variant = getattr(action, "label_variant", None) - if label_variant and not label: - label_variant = None - - if not label_variant: - orders.append(action) - continue - - if label not in orders: - orders.append(label) - by_variant_label[label].append(action) menu = QtWidgets.QMenu(self) actions_mapping = {} - for action_item in orders: - actions = by_variant_label.get(action_item) - if not actions: - action = action_item - elif len(actions) == 1: - action = actions[0] - else: - action = None - - if action: + if is_variant_group: + for action in actions: menu_action = QtWidgets.QAction( lib.get_action_label(action) ) menu.addAction(menu_action) actions_mapping[menu_action] = action - continue - - sub_menu = QtWidgets.QMenu(label, menu) + else: + by_variant_label = collections.defaultdict(list) + orders = [] for action in actions: - menu_action = QtWidgets.QAction( - lib.get_action_label(action) - ) - sub_menu.addAction(menu_action) - actions_mapping[menu_action] = action + # Lable variants + label = getattr(action, "label", None) + label_variant = getattr(action, "label_variant", None) + if label_variant and not label: + label_variant = None - menu.addMenu(sub_menu) + if not label_variant: + orders.append(action) + continue + + if label not in orders: + orders.append(label) + by_variant_label[label].append(action) + + for action_item in orders: + actions = by_variant_label.get(action_item) + if not actions: + action = action_item + elif len(actions) == 1: + action = actions[0] + else: + action = None + + if action: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + continue + + sub_menu = QtWidgets.QMenu(label, menu) + for action in actions: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + sub_menu.addAction(menu_action) + actions_mapping[menu_action] = action + + menu.addMenu(sub_menu) result = menu.exec_(QtGui.QCursor.pos()) if result: From 3b18458cac54e7c99b5e484f1f9508b767085ccb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:39:22 +0200 Subject: [PATCH 30/31] fixed action history --- pype/tools/launcher/window.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/pype/tools/launcher/window.py b/pype/tools/launcher/window.py index b53b5b415c..70a8b0fc2e 100644 --- a/pype/tools/launcher/window.py +++ b/pype/tools/launcher/window.py @@ -317,18 +317,6 @@ class LauncherWindow(QtWidgets.QDialog): self.asset_panel = asset_panel self.actions_bar = actions_bar self.action_history = action_history - - self.data = { - "pages": { - "project": project_panel, - "asset": asset_panel - }, - "model": { - "actions": actions_bar, - "action_history": action_history - }, - } - self.page_slider = page_slider self._page = 0 @@ -458,10 +446,14 @@ class LauncherWindow(QtWidgets.QDialog): if project_name: # Force the "in project" view. - self.pages.slide_view(1, direction="right") - index = self.asset_panel.project_bar.view.findText(project_name) + self.page_slider.slide_view(1, direction="right") + index = self.asset_panel.project_bar.project_combobox.findText( + project_name + ) if index >= 0: - self.asset_panel.project_bar.view.setCurrentIndex(index) + self.asset_panel.project_bar.project_combobox.setCurrentIndex( + index + ) if silo: self.asset_panel.assets_widget.set_silo(silo) @@ -472,4 +464,4 @@ class LauncherWindow(QtWidgets.QDialog): if task_name: # requires a forced refresh first self.asset_panel.on_asset_changed() - self.asset_panel.assets_widget.select_task(task_name) + self.asset_panel.tasks_widget.select_task(task_name) From b2cbfe98d934be40f219377000feb3d30b583ed3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:42:05 +0200 Subject: [PATCH 31/31] reverse logic of action history --- pype/tools/launcher/window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/window.py b/pype/tools/launcher/window.py index 70a8b0fc2e..13b4abee6e 100644 --- a/pype/tools/launcher/window.py +++ b/pype/tools/launcher/window.py @@ -401,11 +401,11 @@ class LauncherWindow(QtWidgets.QDialog): is_control_down = QtCore.Qt.ControlModifier & modifiers if is_control_down: - # User is holding control, rerun the action - self.run_action(action, session=session) - else: # Revert to that "session" location self.set_session(session) + else: + # User is holding control, rerun the action + self.run_action(action, session=session) def get_current_session(self): if self._page == 1: