mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
init commit
This commit is contained in:
parent
76a6ac6bd6
commit
a27eab6049
8 changed files with 1910 additions and 0 deletions
10
pype/tools/launcher/__init__.py
Normal file
10
pype/tools/launcher/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
from .app import (
|
||||
show,
|
||||
cli
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"show",
|
||||
"cli",
|
||||
]
|
||||
5
pype/tools/launcher/__main__.py
Normal file
5
pype/tools/launcher/__main__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from app import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(cli(sys.argv[1:]))
|
||||
95
pype/tools/launcher/actions.py
Normal file
95
pype/tools/launcher/actions.py
Normal file
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
717
pype/tools/launcher/app.py
Normal file
717
pype/tools/launcher/app.py
Normal file
|
|
@ -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()
|
||||
305
pype/tools/launcher/flickcharm.py
Normal file
305
pype/tools/launcher/flickcharm.py
Normal file
|
|
@ -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
|
||||
67
pype/tools/launcher/lib.py
Normal file
67
pype/tools/launcher/lib.py
Normal file
|
|
@ -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
|
||||
292
pype/tools/launcher/models.py
Normal file
292
pype/tools/launcher/models.py
Normal file
|
|
@ -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
|
||||
419
pype/tools/launcher/widgets.py
Normal file
419
pype/tools/launcher/widgets.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue