init commit

This commit is contained in:
iLLiCiTiT 2020-08-10 21:41:33 +02:00
parent 76a6ac6bd6
commit a27eab6049
8 changed files with 1910 additions and 0 deletions

View file

@ -0,0 +1,10 @@
from .app import (
show,
cli
)
__all__ = [
"show",
"cli",
]

View file

@ -0,0 +1,5 @@
from app import cli
if __name__ == '__main__':
import sys
sys.exit(cli(sys.argv[1:]))

View 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
View 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()

View 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

View 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

View 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

View 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()