Merge pull request #209 from pypeclub/feature/205-move_tray_to_pype

Feature/205 move tray to pype
This commit is contained in:
Milan Kolar 2020-06-07 19:36:01 +02:00 committed by GitHub
commit 6f8b9b9b6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 555 additions and 12 deletions

View file

@ -1,7 +1,7 @@
import os
import threading
from pype.api import Logger
from pypeapp import style
from avalon import style
from Qt import QtWidgets
from . import ClockifySettings, ClockifyAPI, MessageWidget

View file

@ -1,5 +1,5 @@
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style
from avalon import style
class MessageWidget(QtWidgets.QWidget):

View file

@ -1,6 +1,6 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style
from avalon import style
class ClockifySettings(QtWidgets.QWidget):

View file

@ -1,6 +1,6 @@
import os
import requests
from pypeapp import style
from avalon import style
from pype.modules.ftrack import credentials
from . import login_tools
from Qt import QtCore, QtGui, QtWidgets

View file

@ -1,6 +1,6 @@
from Qt import QtWidgets, QtCore
from .widgets import LogsWidget, LogDetailWidget
from pypeapp import style
from avalon import style
class LogsWindow(QtWidgets.QWidget):

View file

@ -1,5 +1,5 @@
import appdirs
from pypeapp import style
from avalon import style
from Qt import QtWidgets
import os
import json

View file

@ -1,6 +1,6 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style
from avalon import style
class MusterLogin(QtWidgets.QWidget):

View file

@ -1,7 +1,7 @@
import os
from . import QtCore, QtGui, QtWidgets
from . import get_resource
from pypeapp import style
from avalon import style
class ComponentItem(QtWidgets.QFrame):

View file

@ -1,5 +1,5 @@
from pype.api import Logger
from pypeapp import style
from avalon import style
from Qt import QtCore, QtGui, QtWidgets

View file

@ -1,6 +1,6 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style, resources
from pype.resources import get_resource
from avalon import style
class UserWidget(QtWidgets.QWidget):
@ -14,7 +14,7 @@ class UserWidget(QtWidgets.QWidget):
self.module = module
# Style
icon = QtGui.QIcon(resources.get_resource("icon.png"))
icon = QtGui.QIcon(get_resource("icon.png"))
self.setWindowIcon(icon)
self.setWindowTitle("Username Settings")
self.setMinimumWidth(self.MIN_WIDTH)

View file

@ -0,0 +1,16 @@
import os
def get_resource(*args):
""" Serves to simple resources access
:param *args: should contain *subfolder* names and *filename* of
resource from resources folder
:type *args: list
"""
return os.path.normpath(
os.path.join(
os.path.dirname(__file__),
*args
)
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
pype/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
pype/resources/icon_dev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
pype/resources/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -0,0 +1,17 @@
<svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="312px" height="312px" viewBox="0 0 40 40" xml:space="preserve">
<path opacity="0.2" fill="#ffa500" d="M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946
s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634
c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z"/>
<path fill="#ffa500" d="M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0
C22.32,8.481,24.301,9.057,26.013,10.047z">
<animateTransform attributeType="xml"
attributeName="transform"
type="rotate"
from="00 20.2 20.1"
to="360 20.2 20.1"
dur="0.5s"
repeatCount="indefinite"/>
</path>
<text x="3" y="23" fill="#ffa500" font-style="bold" font-size="7px" font-family="sans-serif">Working...</text>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,4 @@
import sys
import pype_tray
sys.exit(pype_tray.PypeTrayApplication().exec_())

View file

@ -0,0 +1,506 @@
import os
import sys
import platform
from avalon import style
from Qt import QtCore, QtGui, QtWidgets, QtSvg
from pype.resources import get_resource
from pype.api import config, Logger
class TrayManager:
"""Cares about context of application.
Load submenus, actions, separators and modules into tray's context.
"""
modules = {}
services = {}
services_submenu = None
errors = []
items = (
config.get_presets(first_run=True)
.get('tray', {})
.get('menu_items', [])
)
available_sourcetypes = ['python', 'file']
def __init__(self, tray_widget, main_window):
self.tray_widget = tray_widget
self.main_window = main_window
self.log = Logger().get_logger(self.__class__.__name__)
self.icon_run = QtGui.QIcon(get_resource('circle_green.png'))
self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png'))
self.icon_failed = QtGui.QIcon(get_resource('circle_red.png'))
self.services_thread = None
def process_presets(self):
"""Add modules to tray by presets.
This is start up method for TrayManager. Loads presets and import
modules described in "menu_items.json". In `item_usage` key you can
specify by item's title or import path if you want to import it.
Example of "menu_items.json" file:
{
"item_usage": {
"Statics Server": false
}
}, {
"item_import": [{
"title": "Ftrack",
"type": "module",
"import_path": "pype.ftrack.tray",
"fromlist": ["pype", "ftrack"]
}, {
"title": "Statics Server",
"type": "module",
"import_path": "pype.services.statics_server",
"fromlist": ["pype","services"]
}]
}
In this case `Statics Server` won't be used.
"""
# Backwards compatible presets loading
if isinstance(self.items, list):
items = self.items
else:
items = []
# Get booleans is module should be used
usages = self.items.get("item_usage") or {}
for item in self.items.get("item_import", []):
import_path = item.get("import_path")
title = item.get("title")
item_usage = usages.get(title)
if item_usage is None:
item_usage = usages.get(import_path, True)
if item_usage:
items.append(item)
else:
if not title:
title = import_path
self.log.debug("{} - Module ignored".format(title))
if items:
self.process_items(items, self.tray_widget.menu)
# Add services if they are
if self.services_submenu is not None:
self.tray_widget.menu.addMenu(self.services_submenu)
# Add separator
if items and self.services_submenu is not None:
self.add_separator(self.tray_widget.menu)
# Add Exit action to menu
aExit = QtWidgets.QAction("&Exit", self.tray_widget)
aExit.triggered.connect(self.tray_widget.exit)
self.tray_widget.menu.addAction(aExit)
# Tell each module which modules were imported
self.connect_modules()
self.start_modules()
def process_items(self, items, parent_menu):
""" Loop through items and add them to parent_menu.
:param items: contains dictionary objects representing each item
:type items: list
:param parent_menu: menu where items will be add
:type parent_menu: QtWidgets.QMenu
"""
for item in items:
i_type = item.get('type', None)
result = False
if i_type is None:
continue
elif i_type == 'module':
result = self.add_module(item, parent_menu)
elif i_type == 'action':
result = self.add_action(item, parent_menu)
elif i_type == 'menu':
result = self.add_menu(item, parent_menu)
elif i_type == 'separator':
result = self.add_separator(parent_menu)
if result is False:
self.errors.append(item)
def add_module(self, item, parent_menu):
"""Inicialize object of module and add it to context.
:param item: item from presets containing information about module
:type item: dict
:param parent_menu: menu where module's submenus/actions will be add
:type parent_menu: QtWidgets.QMenu
:returns: success of module implementation
:rtype: bool
REQUIRED KEYS (item):
:import_path (*str*):
- full import path as python's import
- e.g. *"path.to.module"*
:fromlist (*list*):
- subparts of import_path (as from is used)
- e.g. *["path", "to"]*
OPTIONAL KEYS (item):
:title (*str*):
- represents label shown in services menu
- import_path is used if title is not set
- title is not used at all if module is not a service
.. note::
Module is added as **service** if object does not have
*tray_menu* method.
"""
import_path = item.get('import_path', None)
title = item.get('title', import_path)
fromlist = item.get('fromlist', [])
try:
module = __import__(
"{}".format(import_path),
fromlist=fromlist
)
obj = module.tray_init(self.tray_widget, self.main_window)
name = obj.__class__.__name__
if hasattr(obj, 'tray_menu'):
obj.tray_menu(parent_menu)
else:
if self.services_submenu is None:
self.services_submenu = QtWidgets.QMenu(
'Services', self.tray_widget.menu
)
action = QtWidgets.QAction(title, self.services_submenu)
action.setIcon(self.icon_run)
self.services_submenu.addAction(action)
if hasattr(obj, 'set_qaction'):
obj.set_qaction(action, self.icon_failed)
self.modules[name] = obj
self.log.info("{} - Module imported".format(title))
except ImportError as ie:
if self.services_submenu is None:
self.services_submenu = QtWidgets.QMenu(
'Services', self.tray_widget.menu
)
action = QtWidgets.QAction(title, self.services_submenu)
action.setIcon(self.icon_failed)
self.services_submenu.addAction(action)
self.log.warning(
"{} - Module import Error: {}".format(title, str(ie)),
exc_info=True
)
return False
return True
def add_action(self, item, parent_menu):
"""Adds action to parent_menu.
:param item: item from presets containing information about action
:type item: dictionary
:param parent_menu: menu where action will be added
:type parent_menu: QtWidgets.QMenu
:returns: success of adding item to parent_menu
:rtype: bool
REQUIRED KEYS (item):
:title (*str*):
- represents label shown in menu
:sourcetype (*str*):
- type of action *enum["file", "python"]*
:command (*str*):
- filepath to script *(sourcetype=="file")*
- python code as string *(sourcetype=="python")*
OPTIONAL KEYS (item):
:tooltip (*str*):
- will be shown when hover over action
"""
sourcetype = item.get('sourcetype', None)
command = item.get('command', None)
title = item.get('title', '*ERROR*')
tooltip = item.get('tooltip', None)
if sourcetype not in self.available_sourcetypes:
self.log.error('item "{}" has invalid sourcetype'.format(title))
return False
if command is None or command.strip() == '':
self.log.error('item "{}" has invalid command'.format(title))
return False
new_action = QtWidgets.QAction(title, parent_menu)
if tooltip is not None and tooltip.strip() != '':
new_action.setToolTip(tooltip)
if sourcetype == 'python':
new_action.triggered.connect(
lambda: exec(command)
)
elif sourcetype == 'file':
command = os.path.normpath(command)
if '$' in command:
command_items = command.split(os.path.sep)
for i in range(len(command_items)):
if command_items[i].startswith('$'):
# TODO: raise error if environment was not found?
command_items[i] = os.environ.get(
command_items[i].replace('$', ''), command_items[i]
)
command = os.path.sep.join(command_items)
new_action.triggered.connect(
lambda: exec(open(command).read(), globals())
)
parent_menu.addAction(new_action)
def add_menu(self, item, parent_menu):
""" Adds submenu to parent_menu.
:param item: item from presets containing information about menu
:type item: dictionary
:param parent_menu: menu where submenu will be added
:type parent_menu: QtWidgets.QMenu
:returns: success of adding item to parent_menu
:rtype: bool
REQUIRED KEYS (item):
:title (*str*):
- represents label shown in menu
:items (*list*):
- list of submenus / actions / separators / modules *(dict)*
"""
try:
title = item.get('title', None)
if title is None or title.strip() == '':
self.log.error('Missing title in menu from presets')
return False
new_menu = QtWidgets.QMenu(title, parent_menu)
new_menu.setProperty('submenu', 'on')
parent_menu.addMenu(new_menu)
self.process_items(item.get('items', []), new_menu)
return True
except Exception:
return False
def add_separator(self, parent_menu):
""" Adds separator to parent_menu.
:param parent_menu: menu where submenu will be added
:type parent_menu: QtWidgets.QMenu
:returns: success of adding item to parent_menu
:rtype: bool
"""
try:
parent_menu.addSeparator()
return True
except Exception:
return False
def connect_modules(self):
"""Sends all imported modules to imported modules
which have process_modules method.
"""
for obj in self.modules.values():
if hasattr(obj, 'process_modules'):
obj.process_modules(self.modules)
def start_modules(self):
"""Modules which can be modified by another modules and
must be launched after *connect_modules* should have tray_start
to start their process afterwards. (e.g. Ftrack actions)
"""
for obj in self.modules.values():
if hasattr(obj, 'tray_start'):
obj.tray_start()
def on_exit(self):
for obj in self.modules.values():
if hasattr(obj, 'tray_exit'):
try:
obj.tray_exit()
except Exception:
self.log.error("Failed to exit module {}".format(
obj.__class__.__name__
))
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
"""Tray widget.
:param parent: Main widget that cares about all GUIs
:type parent: QtWidgets.QMainWindow
"""
def __init__(self, parent):
if os.getenv("PYPE_DEV"):
icon_file_name = "icon_dev.png"
else:
icon_file_name = "icon.png"
self.icon = QtGui.QIcon(get_resource(icon_file_name))
QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent)
# Store parent - QtWidgets.QMainWindow()
self.parent = parent
# Setup menu in Tray
self.menu = QtWidgets.QMenu()
self.menu.setStyleSheet(style.load_stylesheet())
# Set modules
self.tray_man = TrayManager(self, self.parent)
self.tray_man.process_presets()
# Catch activate event
self.activated.connect(self.on_systray_activated)
# Add menu to Context of SystemTrayIcon
self.setContextMenu(self.menu)
def on_systray_activated(self, reason):
# show contextMenu if left click
if platform.system().lower() == "darwin":
return
if reason == QtWidgets.QSystemTrayIcon.Trigger:
position = QtGui.QCursor().pos()
self.contextMenu().popup(position)
def exit(self):
""" Exit whole application.
- Icon won't stay in tray after exit.
"""
self.hide()
self.tray_man.on_exit()
QtCore.QCoreApplication.exit()
class TrayMainWindow(QtWidgets.QMainWindow):
""" TrayMainWindow is base of Pype application.
Every widget should have set this window as parent because
QSystemTrayIcon widget is not allowed to be a parent of any widget.
:param app: Qt application manages application's control flow
:type app: QtWidgets.QApplication
.. note::
*TrayMainWindow* has ability to show **working** widget.
Calling methods:
- ``show_working()``
- ``hide_working()``
.. todo:: Hide working widget if idle is too long
"""
def __init__(self, app):
super().__init__()
self.app = app
self.set_working_widget()
self.trayIcon = SystemTrayIcon(self)
self.trayIcon.show()
def set_working_widget(self):
image_file = get_resource('working.svg')
img_pix = QtGui.QPixmap(image_file)
if image_file.endswith('.svg'):
widget = QtSvg.QSvgWidget(image_file)
else:
widget = QtWidgets.QLabel()
widget.setPixmap(img_pix)
# Set widget properties
widget.setGeometry(img_pix.rect())
widget.setMask(img_pix.mask())
widget.setWindowFlags(
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint
)
widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.center_widget(widget)
self._working_widget = widget
self.helper = DragAndDropHelper(self._working_widget)
def center_widget(self, widget):
frame_geo = widget.frameGeometry()
screen = self.app.desktop().cursor().pos()
center_point = self.app.desktop().screenGeometry(
self.app.desktop().screenNumber(screen)
).center()
frame_geo.moveCenter(center_point)
widget.move(frame_geo.topLeft())
def show_working(self):
self._working_widget.show()
def hide_working(self):
self.center_widget(self._working_widget)
self._working_widget.hide()
class DragAndDropHelper:
""" Helper adds to widget drag and drop ability
:param widget: Qt Widget where drag and drop ability will be added
"""
def __init__(self, widget):
self.widget = widget
self.widget.mousePressEvent = self.mousePressEvent
self.widget.mouseMoveEvent = self.mouseMoveEvent
self.widget.mouseReleaseEvent = self.mouseReleaseEvent
def mousePressEvent(self, event):
self.__mousePressPos = None
self.__mouseMovePos = None
if event.button() == QtCore.Qt.LeftButton:
self.__mousePressPos = event.globalPos()
self.__mouseMovePos = event.globalPos()
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
# adjust offset from clicked point to origin of widget
currPos = self.widget.mapToGlobal(
self.widget.pos()
)
globalPos = event.globalPos()
diff = globalPos - self.__mouseMovePos
newPos = self.widget.mapFromGlobal(currPos + diff)
self.widget.move(newPos)
self.__mouseMovePos = globalPos
def mouseReleaseEvent(self, event):
if self.__mousePressPos is not None:
moved = event.globalPos() - self.__mousePressPos
if moved.manhattanLength() > 3:
event.ignore()
return
class PypeTrayApplication(QtWidgets.QApplication):
"""Qt application manages application's control flow."""
def __init__(self):
super(self.__class__, self).__init__(sys.argv)
# Allows to close widgets without exiting app
self.setQuitOnLastWindowClosed(False)
# Sets up splash
splash_widget = self.set_splash()
splash_widget.show()
self.processEvents()
self.main_window = TrayMainWindow(self)
splash_widget.hide()
def set_splash(self):
if os.getenv("PYPE_DEV"):
splash_file_name = "splash_dev.png"
else:
splash_file_name = "splash.png"
splash_pix = QtGui.QPixmap(get_resource(splash_file_name))
splash = QtWidgets.QSplashScreen(splash_pix)
splash.setMask(splash_pix.mask())
splash.setEnabled(False)
splash.setWindowFlags(
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint
)
return splash