Source code for shotgun_menus.shotgun_menu
# Copyright (c) 2016 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import re
from sgtk.platform.qt import QtCore, QtGui
from .ui import resources_rc
[docs]class ShotgunMenu(QtGui.QMenu):
"""
A base class with support for easily adding labels and groups of actions
with a consistent styling.
Usage Example::
shotgun_menus = sgtk.platform.import_framework("tk-framework-qtwidgets", "shotgun_menus")
ShotgunMenu = shotgun_menus.ShotgunMenu
# ...
action1 = QtGui.QAction("Action 1", self)
action2 = QtGui.QAction("Action 2", self)
submenu = QtGui.QMenu("Submenu", self)
menu = ShotgunMenu(self)
menu.add_group([action1, action2, submenu], "My Actions")
.. image:: images/shotgun_menus_example.png
Image shows the results of the ``ShotgunMenu`` created in the example.
"""
def __init__(self, parent=None):
"""
Initialize the menu.
:param parent: The menu's parent.
:type parent: :class:`~PySide.QtGui.QWidget`
"""
super(ShotgunMenu, self).__init__(parent)
self._typed_text = ""
# create a single shot timer to clear any typed text string after a
# second. this allows the user to search for something in the menu but
# clears the text so they can start over if need be.
self._type_timer = QtCore.QTimer(self)
self._type_timer.setSingleShot(True)
self._type_timer.setInterval(1000)
self._type_timer.timeout.connect(self._on_type_timer_timeout)
# styling to resemble PTR web menus
self.setStyleSheet(
"""
QMenu {
/*
* Ensure the menu only takes up one column and scrolls rather
* than expanding horizontally (the default)
*/
menu-scrollable: 1;
background: palette(window);
padding: 0px 1px 1px 0px;
margin: 0px;
}
QMenu::scroller {
height: 16px;
}
QMenu::item {
padding: 2px 22px 2px 22px;
}
QMenu::item:selected {
border-color: none;
background: palette(midlight);
}
QMenu::separator {
height: 1px;
background: palette(base);
margin-left: 0px;
margin-right: 0px;
margin-top: 4px;
margin-bottom: 0px;
}
QMenu::indicator {
left: 5px;
top: 1px;
}
QMenu::indicator:unchecked {
image: none;
}
QMenu::indicator:checked {
image: url(:tk_framework_qtwidgets.shotgun_menus/check.png);
}
"""
)
[docs] @staticmethod
def get_label(title):
"""Create the label widget to add to the menu."""
label = QtGui.QLabel()
label.setStyleSheet("margin: 0.4em; color: gray;")
font_style = "text-transform: uppercase;"
label.setText("<font style='%s'>%s</font>" % (font_style, title))
return label
[docs] def add_group(self, items, title=None, separator=True, exclusive=False):
"""
Adds a group of items to the menu.
The list of items can include :class:`~PySide.QtGui.QAction` or
:class:`~PySide.QtGui.QMenu` instances. If a ``title`` is supplied,
a non-clickable label will be added with the supplied text at the top
of the list of items in the menu. By default, a separator will be added
above the group unless ``False`` is supplied for the optional ``separator``
argument. A separator will not be included if the group is added to an
empty menu.
A list of all actions, including separator, label, and menu actions,
in the order added, will be returned.
:param list items: A list of actions and/or menus to add to this menu.
:param str title: Optional text to use in a label at the top of the
group.
:param bool separator: Add a separator if ``True`` (default), don't add
if ``False``.
:param bool exclusive: If exclusive is set to ``True``, the added items
will be an exclusive group. If the items are checkable, only one
will be checkable at any given time. The default is ``False``.
:returns: A list of added :class:`~PySide.QtGui.QAction` objects
:rtype: :obj:`list`
"""
action_group = QtGui.QActionGroup(self)
action_group.setExclusive(exclusive)
added_actions = []
if not self.isEmpty() and separator:
added_actions.append(self.addSeparator())
if title:
added_actions.append(self.add_label(title))
for item in items:
if isinstance(item, QtGui.QMenu):
added_actions.append(self.addMenu(item))
else:
self.addAction(item)
added_actions.append(item)
action_group.addAction(item)
return added_actions
[docs] def add_label(self, title):
"""
Add a label with the given title to the menu
:param str title: The title of the sectional label
"""
label = self.get_label(title)
action = QtGui.QWidgetAction(self)
action.setDefaultWidget(label)
self.addAction(action)
return action
[docs] def keyReleaseEvent(self, event):
"""Allow users to type menu item names to highlight/select them."""
# stop the timer that clears the typed text
self._type_timer.stop()
# a lowercase string representation of the typed key
event_text = str(event.text()).lower()
# if the user wants to clear a letter, do so.
if event.key() in [QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete]:
if len(self._typed_text):
self._typed_text = self._typed_text[:-1]
# otherwise, see if the letter is something reasonable (space,
# alphanumeric, dash, dot)
elif re.match(r"^[\s\w\-\.]+$", event_text):
# add it to the typed text
self._typed_text += event_text
else:
# the typed key isn't one we recognize for matching. call the
# default implementation
super(ShotgunMenu, self).keyReleaseEvent(event)
self._type_timer.start()
return
# now search the actions to see if one matches the typed text
for action in self.actions():
# use a try to ignore any possible errors
try:
if action.text():
action_text = str(action.text()).lower()
if action_text.startswith(self._typed_text):
# match found. make this the active action
self.setActiveAction(action)
# restart the timer to clear the text after a given period
self._type_timer.start()
return
except Exception as e:
# assume no match
pass
# didn't find a match, call the base class
super(ShotgunMenu, self).keyReleaseEvent(event)
# ensure the timer is started
self._type_timer.start()
def _on_type_timer_timeout(self):
"""Timeout triggered after typing has ceased for a given interval."""
self._typed_text = ""