Source code for shotgun_fields.shotgun_field_editable

# 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 sgtk
from sgtk.platform.qt import QtCore, QtGui
from .ui import resources_rc

# This is the stylesheet applied to edit, apply, and done buttons.
BUTTON_STYLE = "background-color: none; border: none; min-width: 15px;"


[docs]class ShotgunFieldEditable(QtGui.QStackedWidget): """ Wraps ``DISPLAY`` and ``EDITOR`` widgets into a :class:`~PySide.QtGui.QStackedWidget` instance to allow toggling between the two modes. The class exposes the same public interface as individual field display and editor widgets. """ # forwarded from either of the enclosed widgets values changing value_changed = QtCore.Signal() def __init__(self, display_widget, editor_widget, parent=None): """ Initialize the editable widget with the display and editor instances. :param display_widget: The ``DISPLAY`` widget instance :type display_widget: :class:`~PySide.QtGui.QWidget` :param editor_widget: The ``EDITOR`` widget instance :type editor_widget: :class:`~PySide.QtGui.QWidget` :param parent: The parent widget or ``None`` :type parent: :class:`~PySide.QtGui.QWidget` """ super(ShotgunFieldEditable, self).__init__(parent) self._enabled = True self._display = _DisplayWidget(display_widget) self._editor = _EditorWidget(editor_widget) self._display_index = self.addWidget(self._display) self._edit_index = self.addWidget(self._editor) # ---- connect signals # user clicked the button to edit the value self._display.edit_requested.connect( lambda: self.setCurrentWidget(self._editor) ) # user is done editing self._editor.done_editing.connect(lambda: self.setCurrentWidget(self._display)) # apply the new value entered into the editor self._editor.edit_widget.value_changed.connect(self._apply_value) # react to the current widget being changed self.currentChanged.connect(self._on_current_changed) # forward the value change signal from the display widget value changing. # because the editor widget currently applies its changed value to the # display widget, this should handle a value change from either widget. # if this widget is modified to wait on applying the new value until # after SG has been update, this logic will need to change. self._display.display_widget.value_changed.connect(self.value_changed.emit)
[docs] def destroy(self): """ Call to ensure proper destruction of contained widgets. """ self._display.display_widget.destroy() self._editor.edit_widget.destroy()
[docs] def enable_editing(self, enable): """ For consistency, allow the calling code to enable/disable editing. Calling this method with a value of ``False`` will force the display widget to be shown and the edit button to be hidden. A value of ``True`` will allow the edit button to be displayed again on mouse over. :param enable: ``True`` if editing should be enabled, ``False`` otherwise. """ self._enabled = enable # if editing turned off, force the display widget to be shown if not self._enabled: self.setCurrentWidget(self._display) # make sure display widget follows self._display.enable_editing(self._enabled)
[docs] def get_entity(self): """ Returns the entity of the widget (display or editor) currently being shown. """ if self.currentWidget() == self._display: return self._display.display_widget.get_entity() else: return self._editor.edit_widget.get_entity()
[docs] def get_entity_type(self): """ Returns the entity type of the widget (display or editor) currently being shown. """ if self.currentWidget() == self._display: return self._display.display_widget.get_entity_type() else: return self._editor.edit_widget.get_entity_type()
[docs] def get_field_name(self): """ Returns the field name of the widget (display or editor) currently being shown. """ if self.currentWidget() == self._display: return self._display.display_widget.get_field_name() else: return self._editor.edit_widget.get_field_name()
[docs] def get_value(self): """ Returns the value of the widget (display or editor) currently being shown. """ if self.currentWidget() == self._display: return self._display.display_widget.get_value() else: return self._editor.edit_widget.get_value()
[docs] def set_value(self, value): """ Set the value of the widget (display or editor) currently being shown. .. note:: Calling this method while the editor is displayed will trigger the value to be accepted and applied. The display widget will automatically be shown. This widget's ``value_changed()`` signal will also be emitted. """ if self.currentWidget() == self._display: return self._display.display_widget.set_value(value) else: return self._editor.edit_widget.set_value(value)
def minimumSizeHint(self): """ Returns the minimum size hint for the currently displayed widget """ return self.currentWidget().minimumSizeHint() def sizeHint(self): """ Returns the size hint for the currently displayed widget """ return self.currentWidget().sizeHint() def _apply_value(self): """ Apply the editor's current value to the display widget and finish editing. """ new_value = self._editor.edit_widget.get_value() self._display.display_widget.set_value(new_value) self.setCurrentWidget(self._display) def _on_current_changed(self, index): """ Primarily used to ensure focus and to make sure the display/edit widgets are in sync. :param int index: The index of the newly current widget in the stack. """ if index == self._edit_index: try: self._editor.edit_widget.blockSignals(True) self._editor.edit_widget.set_value( self._display.display_widget.get_value() ) finally: self._editor.edit_widget.blockSignals(False) if hasattr(self._editor.edit_widget, "_begin_edit"): self._editor.edit_widget._begin_edit() self.currentWidget().setFocus()
[docs]class ShotgunFieldNotEditable(QtGui.QWidget): """ Simplified wrapper that indicates a field is not editable. Adds a "no edit" icon when the supplied ``DISPLAY`` widget is hovered. """ # forward from the wrapped widget value_changed = QtCore.Signal() def __init__(self, display_widget, parent=None): """ Initialize the widget. :param display_widget: The ``DISPLAY`` widget instance :type display_widget: :class:`~PySide.QtGui.QWidget` :param parent: The parent widget or ``None`` :type parent: :class:`~PySide.QtGui.QWidget` """ super(ShotgunFieldNotEditable, self).__init__(parent) self._display_widget = display_widget # this is the "no edit" label that will show on hover self._no_edit_lbl = QtGui.QLabel(self) self._no_edit_lbl.setPixmap( QtGui.QPixmap(":/qtwidgets-shotgun-fields/not_editable.png") ) self._no_edit_lbl.setFixedSize(QtCore.QSize(16, 16)) self._no_edit_lbl.hide() spacer = QtGui.QWidget() spacer.setFixedHeight(self._no_edit_lbl.height()) spacer.setFixedWidth(4) layout = QtGui.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(display_widget) layout.addWidget(spacer) layout.addWidget(self._no_edit_lbl) layout.addStretch(10) self.installEventFilter(self) # forward the display widget's value changed signal self._display_widget.value_changed.connect(self.value_changed.emit)
[docs] def enable_editing(self, enable): """ This method exists to allow this object to conform to the ``EDITABLE`` widget protocol. Because this is a field that isn't editable however, this method does nothing. """ pass
def eventFilter(self, obj, event): """ Filter mouse enter/leave events in order to show/hide the "no edit" label. """ if event.type() == QtCore.QEvent.Enter: self._no_edit_lbl.show() elif event.type() == QtCore.QEvent.Leave: self._no_edit_lbl.hide() return False def __getattr__(self, name): """ Routes any attributes not found on the widget to the fields widget that it is wrapping. """ return getattr(self._display_widget, name)
class _DisplayWidget(QtGui.QWidget): """ A wrapper around a display widget with a hoverable "edit" button. """ edit_requested = QtCore.Signal() def __init__(self, display_widget, parent=None): """ Initialize the wrapper widget. :param display_widget: The ``DISPLAY`` widget instance :type display_widget: :class:`~PySide.QtGui.QWidget` :param parent: The parent widget instance or None :type parent: :class:`~PySide.QtGui.QWidget` :return: """ super(_DisplayWidget, self).__init__(parent) self._enabled = True self._display_widget = display_widget self._edit_btn = QtGui.QPushButton() self._edit_btn.setIcon(QtGui.QIcon(":/qtwidgets-shotgun-fields/edit_field.png")) self._edit_btn.setFixedSize(QtCore.QSize(16, 16)) self._edit_btn.setFocusPolicy(QtCore.Qt.NoFocus) self._edit_btn.hide() # make sure there's never a bg color or border self._edit_btn.setStyleSheet(BUTTON_STYLE) spacer = QtGui.QWidget() spacer.setFixedHeight(self._edit_btn.height()) spacer.setFixedWidth(4) layout = QtGui.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(display_widget) layout.addWidget(spacer) layout.addWidget(self._edit_btn) layout.addStretch(10) self.setMinimumHeight(self._edit_btn.height()) self.installEventFilter(self) # ---- connect singals self._edit_btn.clicked.connect(lambda: self.edit_requested.emit()) def eventFilter(self, obj, event): """ Filter out mouse enter/leave events in order to show/hide the edit button. """ # only worry about showing the edit button if editing is enabled if self._enabled: if event.type() == QtCore.QEvent.Enter: self._edit_btn.show() elif event.type() == QtCore.QEvent.Leave: self._edit_btn.hide() return False def enable_editing(self, enable): """ Calling this method with a value of ``False`` will force the edit button to be hidden. A value of ``True`` will allow the edit button to be displayed again on mouse over. :param enable: ``True`` if editing should be enabled, ``False`` otherwise. """ self._enabled = enable # force hide the edit button if editing turned off if not self._enabled: self._edit_btn.hide() @property def display_widget(self): """Convenience property to access the display widget""" return self._display_widget class _EditorWidget(QtGui.QWidget): """ Wrapper around the editor widget to display "done" and "apply" buttons :signal: ``done_editing()`` emitted when the editor is ready to be closed """ done_editing = QtCore.Signal() def __init__(self, editor_widget, parent=None): """ Initialize the wrapper widget. :param editor_widget: The ``EDITOR`` widget instance :type editor_widget: :class:`~PySide.QtGui.QWidget` :param parent: The parent widget instance or None :type parent: :class:`~PySide.QtGui.QWidget` :return: """ super(_EditorWidget, self).__init__(parent) self._editor_widget = editor_widget self._editor_widget.setFocusPolicy(QtCore.Qt.StrongFocus) self._done_btn = QtGui.QPushButton() self._done_btn.setIcon(QtGui.QIcon(":/qtwidgets-shotgun-fields/edit_close.png")) self._done_btn.setFixedSize(QtCore.QSize(16, 16)) self._done_btn.setFocusPolicy(QtCore.Qt.NoFocus) self._apply_btn = QtGui.QPushButton() self._apply_btn.setIcon( QtGui.QIcon(":/qtwidgets-shotgun-fields/apply_value.png") ) self._apply_btn.setFixedSize(QtCore.QSize(16, 16)) self._apply_btn.setFocusPolicy(QtCore.Qt.NoFocus) # make sure there's never a bg color or border self._done_btn.setStyleSheet(BUTTON_STYLE) self._apply_btn.setStyleSheet(BUTTON_STYLE) if self._editor_widget.sizeHint().height() >= 32: btn_layout = QtGui.QVBoxLayout() btn_layout.addWidget(self._done_btn) btn_layout.addStretch() btn_layout.addWidget(self._apply_btn) else: btn_layout = QtGui.QHBoxLayout() btn_layout.addWidget(self._apply_btn) btn_layout.addWidget(self._done_btn) btn_layout.addStretch() if getattr(editor_widget, "_IMMEDIATE_APPLY", None): # widget is set to immediately apply value. no need to display the btn self._apply_btn.hide() layout = QtGui.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) layout.addWidget(editor_widget) layout.addLayout(btn_layout) layout.addStretch() layout.setAlignment(self._done_btn, QtCore.Qt.AlignBottom) self.installEventFilter(self) # ---- connect singals self._done_btn.clicked.connect(lambda: self.done_editing.emit()) self._apply_btn.clicked.connect(self._apply_value) def eventFilter(self, obj, event): """ Capture the Escape key to emit the ``done_editing`` signal. """ if event.type() == QtCore.QEvent.KeyPress: if event.key() == QtCore.Qt.Key_Escape: self.done_editing.emit() return True return False def setFocus(self): """ Override the default behavior to give focus to the editor widget. """ self._editor_widget.setFocus() @property def edit_widget(self): """Convenience property to access the editor widget""" return self._editor_widget def _apply_value(self): """ Called when the "apply" button is clicked. Make sure the edit widget's value is updated and emit the ``done_editing`` signal. """ # TODO: rather than doing this weird setting of its own value, this # should simply emit a 'value_changed' signal. the widgets should be # responsible for storing their own values as they are modified self.edit_widget.set_value(self.edit_widget.get_value()) self.done_editing.emit()