Ejemplo n.º 1
0
class ShotgunSystemTrayIcon(QtGui.QSystemTrayIcon):
    """ wrapper around system tray icon """

    clicked = QtCore.Signal()
    double_clicked = QtCore.Signal()
    right_clicked = QtCore.Signal()

    def __init__(self, parent=None):
        QtGui.QSystemTrayIcon.__init__(self, parent)

        # configure the system tray icon
        icon = QtGui.QIcon(":/tk-desktop/default_systray_icon")
        self.setIcon(icon)
        self.setToolTip("Shotgun")

        # connect up signal handlers
        self.activated.connect(self.__activated)

    def __activated(self, reason):
        if reason == QtGui.QSystemTrayIcon.Trigger:
            self.clicked.emit()
        elif reason == QtGui.QSystemTrayIcon.DoubleClick:
            self.double_clicked.emit()
        elif reason == QtGui.QSystemTrayIcon.Context:
            self.right_clicked.emit()
Ejemplo n.º 2
0
class ListBase(QtGui.QWidget):

    clicked = QtCore.Signal(QtGui.QWidget)
    double_clicked = QtCore.Signal(QtGui.QWidget)

    def __init__(self, app, worker, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self._app = app
        self._worker = worker
        self.ui = self._setup_ui()

    def supports_selection(self):
        return False

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            # handle this event!
            self.clicked.emit(self)

    def mouseDoubleClickEvent(self, event):
        self.double_clicked.emit(self)

    def set_selected(self, status):
        pass

    def is_selected(self):
        return False

    def set_title(self, title):
        pass

    def set_details(self, text):
        pass

    def get_title(self):
        return None

    def get_details(self):
        return None

    def _setup_ui(self):
        """
        Setup the Qt UI.  Typically, this just instantiates the UI class
        and calls its .setupUi(self) method.

        This can be overriden in child classes - this provides a simple
        mechanism to replace the item UI with a custom version if needed

        :returns:    The constructed QWidget
        """
        raise NotImplementedError()
Ejemplo n.º 3
0
class Task(QtCore.QObject):
    """
    This is a wrapper class which allows us to run tank commands
    inside the QT universe. This approach is handy when an engine needs
    to start up a qt event loop as part of its initailization.
    """
    finished = QtCore.Signal()

    def __init__(self, engine, callback):
        QtCore.QObject.__init__(self)
        self._callback = callback
        self._engine = engine

    def run_command(self):
        # execute the callback

        # note that because pyside has its own exception wrapper around
        # exec we need to catch and log any exceptions here.
        try:
            self._callback()

        except tank.TankError, e:
            self._engine.log_error(str(e))

        except Exception:
            self._engine.log_exception("A general error was reported.")
Ejemplo n.º 4
0
class Task(QtCore.QObject):
    """
    This is a wrapper class which allows us to run tank commands
    inside the QT universe. This approach is handy when an engine needs
    to start up a qt event loop as part of its initailization.
    """
    finished = QtCore.Signal()

    def __init__(self, engine, callback, args):
        QtCore.QObject.__init__(self)
        self._callback = callback
        self._args = args
        self._engine = engine

    def run_command(self):

        try:
            # execute the callback
            self._callback(*self._args)

        except tank.TankError, e:
            self._engine.log_error(str(e))

        except KeyboardInterrupt:
            self._engine.log_info("The operation was cancelled by the user.")
Ejemplo n.º 5
0
class ListBase(QtGui.QWidget):
    
    clicked = QtCore.Signal(QtGui.QWidget)
    double_clicked = QtCore.Signal(QtGui.QWidget)
        
    
    def __init__(self, app, worker, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self._app = app
        self._worker = worker

    def supports_selection(self):
        return False

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            # handle this event!
            self.clicked.emit(self)

    def mouseDoubleClickEvent(self, event):
        self.double_clicked.emit(self)

    def set_selected(self, status):
        pass
    
    def is_selected(self):
        return False
        
    def set_title(self, title):
        pass

    def set_details(self, text):
        pass
        
    def get_title(self):
        return None

    def get_details(self):
        return None
Ejemplo n.º 6
0
            class DockWidget(QtGui.QDockWidget):
                """
                Widget used for docking app panels that ensures the widget is closed when the
                dock is closed
                """

                closed = QtCore.Signal(QtCore.QObject)

                def closeEvent(self, event):
                    widget = self.widget()
                    if widget:
                        widget.close()
                    self.closed.emit(self)
class ResizeEventFilter(QtCore.QObject):
    """
    Event filter which emits a resized signal whenever
    the monitored widget resizes
    """
    resized = QtCore.Signal()

    def eventFilter(self, obj, event):
        # peek at the message
        if event.type() == QtCore.QEvent.Resize:
            # re-broadcast any resize events
            self.resized.emit()
        # pass it on!
        return False
class ResizeEventFilter(QtCore.QObject):
    """
    Event filter which emits a resized signal whenever
    the monitored widget resizes. This is so that the overlay wrapper
    class can be informed whenever the Widget gets a resize event.
    """
    resized = QtCore.Signal()

    def eventFilter(self, obj, event):
        # peek at the message
        if event.type() == QtCore.QEvent.Resize:
            # re-broadcast any resize events
            self.resized.emit()
        # pass it on!
        return False
Ejemplo n.º 9
0
class QtTask(QtCore.QObject):
    """
    QT dialogue wrapper
    """
    finished = QtCore.Signal()

    def __init__(self, title, engine, widget_class, args, kwargs):
        """
        Constructor
        """
        QtCore.QObject.__init__(self)
        self._title = title
        self._engine = engine
        self._widget_class = widget_class
        self._args = args
        self._kwargs = kwargs
        self._return_data = None

    def run_command(self):
        """
        Execute the payload of the task. Emit finished signal at the end.
        """
        try:
            # execute the callback
            self._return_data = self._engine.show_modal(
                self._title, self._engine, self._widget_class, *self._args,
                **self._kwargs)

        except KeyboardInterrupt:
            self._engine.log_info("The operation was cancelled by the user.")

        finally:
            # broadcast that we have finished this command
            self.finished.emit()

    def get_return_data(self):
        """
        Return the return value from the modal dialog
        """
        return self._return_data
Ejemplo n.º 10
0
class Task(QtCore.QObject):
    """
    This is a wrapper class which allows us to run tank commands
    inside the QT universe. This approach is handy when an engine needs
    to start up a qt event loop as part of its initailization.
    """

    finished = QtCore.Signal()

    def __init__(self, engine, callback, args):
        QtCore.QObject.__init__(self)
        self._callback = callback
        self._args = args
        self._engine = engine

    def run_command(self):

        try:
            # execute the callback
            self._callback(*self._args)

        except tank.TankError as e:
            self._engine.log_error(str(e))

        except KeyboardInterrupt:
            self._engine.log_info("The operation was cancelled by the user.")

        except Exception:
            self._engine.log_exception("A general error was reported.")

        finally:
            # broadcast that we have finished this command
            if not self._engine.has_received_ui_creation_requests():
                # while the app has been doing its thing, no UIs were
                # created (at least not any tank UIs) - assume it is a
                # console style app and that the end of its callback
                # execution means that it is complete and that we should return
                self.finished.emit()
Ejemplo n.º 11
0
class Worker(QtCore.QThread):

    work_completed = QtCore.Signal(str, dict)
    work_failure = QtCore.Signal(str, str)

    def __init__(self, app, parent=None):
        QtCore.QThread.__init__(self, parent)
        self._execute_tasks = True
        self._app = app
        self._queue_mutex = QtCore.QMutex()
        self._queue = []
        self._receivers = {}

    def stop(self):
        """
        Stops the worker, run this before shutdown
        """
        self._execute_tasks = False

    def clear(self):
        """
        Empties the queue
        """
        self._queue_mutex.lock()
        try:
            self._queue = []
        finally:
            self._queue_mutex.unlock()

    def queue_work(self, worker_fn, params, asap=False):
        """
        Queues up some work.
        Returns a unique identifier to identify this item 
        """
        uid = uuid.uuid4().hex

        work = {"id": uid, "fn": worker_fn, "params": params}
        self._queue_mutex.lock()
        try:
            if asap:
                # first in the queue
                self._queue.insert(0, work)
            else:
                self._queue.append(work)
        finally:
            self._queue_mutex.unlock()

        return uid

    ############################################################################################
    #

    def run(self):

        while self._execute_tasks:

            self._queue_mutex.lock()
            try:
                queue_len = len(self._queue)
            finally:
                self._queue_mutex.unlock()

            if queue_len == 0:
                # polling. TODO: replace with semaphor!
                self.msleep(200)

            else:
                # pop queue
                self._queue_mutex.lock()
                try:
                    item_to_process = self._queue.pop(0)
                finally:
                    self._queue_mutex.unlock()

                data = None
                try:
                    data = item_to_process["fn"](item_to_process["params"])
                except Exception, e:
                    if self._execute_tasks:
                        self.work_failure.emit(item_to_process["id"],
                                               "An error occured: %s" % e)

                else:
                    if self._execute_tasks:
                        self.work_completed.emit(item_to_process["id"], data)
Ejemplo n.º 12
0
class BrowserWidget(QtGui.QWidget):

    ######################################################################################
    # SIGNALS

    # when the selection changes
    selection_changed = QtCore.Signal()

    # when someone double clicks on an item
    action_requested = QtCore.Signal()

    ######################################################################################
    # Init & Destruct

    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        # set up the UI
        self.ui = Ui_Browser()
        self.ui.setupUi(self)

        # hide the overlays
        self.ui.load_overlay.setVisible(False)
        self.ui.message_overlay.setVisible(False)

        self._app = None
        self._worker = None
        self._current_work_id = None
        self._selected_item = None
        self._selected_items = []
        self._dynamic_widgets = []
        self._multi_select = False
        self._search = True

        # spinner
        self._spin_icons = []
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_1.png"))
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_2.png"))
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_3.png"))
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_4.png"))
        self._timer = QtCore.QTimer(self)
        self._timer.timeout.connect(self._update_spinner)
        self._current_spinner_index = 0

        # search
        self.ui.search.textEdited.connect(self._on_search_text_changed)

    def enable_multi_select(self, status):
        """
        Should we enable multi select
        """
        self._multi_select = True

    def enable_search(self, status):
        """
        Toggle the search bar (on by default)
        """
        self.ui.search.setVisible(status)

    def destroy(self):
        if self._worker:
            self._worker.stop()

    def set_app(self, app):
        """
        associate with an app object
        """
        self._app = app
        # set up worker queue
        self._worker = Worker(app)
        self._worker.work_completed.connect(self._on_worker_signal)
        self._worker.work_failure.connect(self._on_worker_failure)

        self._worker.start()

    def set_label(self, label):
        """
        Sets the text next to the search button 
        """
        self.ui.label.setText("<big>%s</big>" % label)

    ######################################################################################
    # Public Methods

    def load(self, data):
        """
        Loads data into the browser widget. 
        Called by outside code 
        """
        # start spinning
        self.ui.scroll_area.setVisible(False)
        self.ui.load_overlay.setVisible(True)
        self._timer.start(100)
        # queue up work
        self._current_work_id = self._worker.queue_work(self.get_data,
                                                        data,
                                                        asap=True)

    def clear(self):
        """
        Clear widget of its contents.
        """
        # hide overlays
        self.ui.load_overlay.setVisible(False)
        self.ui.message_overlay.setVisible(False)
        self.ui.scroll_area.setVisible(True)

        # clear search box
        self.ui.search.setText("")

        # also reset any jobs that are processing. No point processing them
        # if their requestors are gone.
        if self._worker:
            self._worker.clear()

        for x in self._dynamic_widgets:
            self.ui.scroll_area_layout.removeWidget(x)
            x.deleteLater()
        self._dynamic_widgets = []

        # lastly, clear selection
        self.clear_selection()

    def set_message(self, message):
        """
        Replace the list of items with a single message
        """
        self.ui.load_overlay.setVisible(False)
        self.ui.message_overlay.setVisible(True)
        self.ui.scroll_area.setVisible(False)
        self.ui.status_message.setText(message)

    def clear_selection(self):
        """
        Clears the selection
        """
        for x in self._dynamic_widgets:
            x.set_selected(False)
        self._selected_item = None
        self._selected_items = []

    def get_selected_item(self):
        """
        Gets the last selected item, None if no selection
        """
        return self._selected_item

    def get_selected_items(self):
        """
        Returns entire selection
        """
        return self._selected_items

    def get_items(self):
        return self._dynamic_widgets

    def select(self, item):
        self._on_item_clicked(item)
        # in order for the scroll to happen during load, first give
        # the scroll area  chance to resize it self by processing its event queue.
        QtCore.QCoreApplication.processEvents()
        # and focus on the selection
        self.ui.scroll_area.ensureWidgetVisible(item)

    ##########################################################################################
    # Protected stuff - implemented by deriving classes

    def get_data(self, data):
        """
        Needs to be implemented by subclasses
        """
        raise Exception("not implemented!")

    def process_result(self, result):
        """
        Needs to be implemented by subclasses
        """
        raise Exception("not implemented!")

    ##########################################################################################
    # Internals

    def _on_search_text_changed(self, text):
        """
        Cull based on search box
        """

        if text == "":
            # show all items
            for i in self._dynamic_widgets:
                i.setVisible(True)

        elif len(text) > 2:  # cull by string for strings > 2 chars

            # if running PyQt, convert QString to str
            if not isinstance(text, basestring):
                # convert QString to str
                text = str(text)

            # now we have a str or unicode object which has the lower() method
            lower_text = text.lower()

            for i in self._dynamic_widgets:

                details = i.get_details()

                # if running PyQt, convert QString to str
                details_lower = details
                if not isinstance(details_lower, basestring):
                    details_lower = str(details_lower)
                # now we have a str or unicode object which has the lower() method
                details_lower = details_lower.lower()

                if details is None:  # header
                    i.setVisible(True)

                elif lower_text in details_lower:
                    i.setVisible(True)

                else:
                    i.setVisible(False)

    def _on_worker_failure(self, uid, msg):
        """
        The worker couldn't execute stuff
        """
        if self._current_work_id != uid:
            # not our job. ignore
            return

        # finally, turn off progress indication and turn on display
        self.ui.scroll_area.setVisible(True)
        self.ui.load_overlay.setVisible(False)
        self._timer.stop()

        # show error message
        self.set_message(msg)

    def _on_worker_signal(self, uid, data):
        """
        Signalled whenever the worker completes something
        """
        if self._current_work_id != uid:
            # not our job. ignore
            return

        # finally, turn off progress indication and turn on display
        self.ui.scroll_area.setVisible(True)
        self.ui.load_overlay.setVisible(False)
        self._timer.stop()

        # process!
        self.process_result(data)

    def _update_spinner(self):
        """
        Animate spinner icon
        """
        self.ui.progress_bar.setPixmap(
            self._spin_icons[self._current_spinner_index])
        self._current_spinner_index += 1
        if self._current_spinner_index == 4:
            self._current_spinner_index = 0

    def _on_item_clicked(self, item):

        if item.supports_selection() == False:
            # not all items are selectable
            return

        if self._multi_select:
            if item.is_selected():
                # remove from selection
                item.set_selected(False)
                # remove it from list of selected items
                s = set(self._selected_items) - set([item])
                self._selected_items = list(s)
                if len(self._selected_items) > 0:
                    self._selected_item = self._selected_items[0]
                else:
                    self._selected_item = None
            else:
                # add to selection
                item.set_selected(True)
                self._selected_item = item
                self._selected_items.append(item)
        else:
            # single select
            self.clear_selection()
            item.set_selected(True)
            self._selected_item = item
            self._selected_items = [item]

        self.selection_changed.emit()

    def _on_item_double_clicked(self, item):
        self.action_requested.emit()

    def add_item(self, item_class):
        """
        Adds a list item. Returns the created object.
        """
        widget = item_class(self._app, self._worker, self)
        self.ui.scroll_area_layout.addWidget(widget)
        self._dynamic_widgets.append(widget)
        widget.clicked.connect(self._on_item_clicked)
        widget.double_clicked.connect(self._on_item_double_clicked)
        return widget
Ejemplo n.º 13
0
class ThumbnailWidget(QtGui.QWidget):
    """
    Thumbnail widget that provides screen capture functionality
    """

    thumbnail_changed = QtCore.Signal()

    def __init__(self, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)

        self._ui = Ui_ThumbnailWidget()
        self._ui.setupUi(self)

        # create layout to control buttons frame
        layout = QtGui.QHBoxLayout()
        layout.addWidget(self._ui.buttons_frame)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.setLayout(layout)

        # connect to buttons:
        self._ui.camera_btn.clicked.connect(self._on_camera_clicked)

        self._btns_transition_anim = None
        self._update_ui()

    # @property
    def _get_thumbnail(self):
        pm = self._ui.thumbnail.pixmap()
        return pm if pm and not pm.isNull() else None

    # @thumbnail.setter
    def _set_thumbnail(self, value):
        self._ui.thumbnail.setPixmap(value if value else QtGui.QPixmap())
        self._update_ui()
        self.thumbnail_changed.emit()

    thumbnail = property(_get_thumbnail, _set_thumbnail)

    def enable_screen_capture(self, enable):
        self._ui.camera_btn.setVisible(enable)

    def resizeEvent(self, event):
        self._update_ui()

    def enterEvent(self, event):
        """
        when the cursor enters the control, show the buttons
        """
        if self.thumbnail and self._are_any_btns_enabled():
            self._ui.buttons_frame.show()
            if hasattr(QtCore, "QAbstractAnimation"):
                self._run_btns_transition_anim(
                    QtCore.QAbstractAnimation.Forward)
            else:
                # Q*Animation classes aren't available so just
                # make sure the button is visible:
                self.btn_visibility = 1.0

    def leaveEvent(self, event):
        """
        when the cursor leaves the control, hide the buttons
        """
        if self.thumbnail and self._are_any_btns_enabled():
            if hasattr(QtCore, "QAbstractAnimation"):
                self._run_btns_transition_anim(
                    QtCore.QAbstractAnimation.Backward)
            else:
                # Q*Animation classes aren't available so just
                # make sure the button is hidden:
                self._ui.buttons_frame.hide()
                self.btn_visibility = 0.0

    def _are_any_btns_enabled(self):
        """
        Return if any of the buttons are enabled
        """
        return not (self._ui.camera_btn.isHidden())

    """
    button visibility property used by QPropertyAnimation
    """

    def get_btn_visibility(self):
        return self._btns_visibility

    def set_btn_visibility(self, value):
        self._btns_visibility = value
        self._ui.buttons_frame.setStyleSheet(
            "#buttons_frame {border-radius: 2px; background-color: rgba(32, 32, 32, %d);}"
            % (64 * value))

    btn_visibility = QtCore.Property(float, get_btn_visibility,
                                     set_btn_visibility)

    def _run_btns_transition_anim(self, direction):
        """
        Run the transition animation for the buttons
        """
        if not self._btns_transition_anim:
            # set up anim:
            self._btns_transition_anim = QtCore.QPropertyAnimation(
                self, "btn_visibility")
            self._btns_transition_anim.setDuration(150)
            self._btns_transition_anim.setStartValue(0.0)
            self._btns_transition_anim.setEndValue(1.0)
            self._btns_transition_anim.finished.connect(
                self._on_btns_transition_anim_finished)

        if self._btns_transition_anim.state(
        ) == QtCore.QAbstractAnimation.Running:
            if self._btns_transition_anim.direction() != direction:
                self._btns_transition_anim.pause()
                self._btns_transition_anim.setDirection(direction)
                self._btns_transition_anim.resume()
            else:
                pass  # just let animation continue!
        else:
            self._btns_transition_anim.setDirection(direction)
            self._btns_transition_anim.start()

    def _on_btns_transition_anim_finished(self):
        if self._btns_transition_anim.direction(
        ) == QtCore.QAbstractAnimation.Backward:
            self._ui.buttons_frame.hide()

    def _on_camera_clicked(self):
        pm = self._on_screenshot()
        if pm:
            self.thumbnail = pm

    def _update_ui(self):

        # maximum size of thumbnail is widget geom:
        thumbnail_geom = self.geometry()
        thumbnail_geom.moveTo(0, 0)
        scale_contents = False

        pm = self.thumbnail
        if pm:
            # work out size thumbnail should be to maximize size
            # whilst retaining aspect ratio
            pm_sz = pm.size()

            h_scale = float(thumbnail_geom.height() - 4) / float(
                pm_sz.height())
            w_scale = float(thumbnail_geom.width() - 4) / float(pm_sz.width())
            scale = min(1.0, h_scale, w_scale)
            scale_contents = (scale < 1.0)

            new_height = min(int(pm_sz.height() * scale),
                             thumbnail_geom.height())
            new_width = min(int(pm_sz.width() * scale), thumbnail_geom.width())

            new_geom = QtCore.QRect(thumbnail_geom)
            new_geom.moveLeft((
                (thumbnail_geom.width() - 4) / 2 - new_width / 2) + 2)
            new_geom.moveTop((
                (thumbnail_geom.height() - 4) / 2 - new_height / 2) + 2)
            new_geom.setWidth(new_width)
            new_geom.setHeight(new_height)
            thumbnail_geom = new_geom

        self._ui.thumbnail.setScaledContents(scale_contents)
        self._ui.thumbnail.setGeometry(thumbnail_geom)

        # now update buttons based on current thumbnail:
        if not self._btns_transition_anim or self._btns_transition_anim.state(
        ) == QtCore.QAbstractAnimation.Stopped:
            if self.thumbnail or not self._are_any_btns_enabled():
                self._ui.buttons_frame.hide()
                self._btns_visibility = 0.0
            else:
                self._ui.buttons_frame.show()
                self._btns_visibility = 1.0

    def _safe_get_dialog(self):
        """
        Get the widgets dialog parent.  
        
        just call self.window() but this is unstable in Nuke
        Previously this would
        causing a crash on exit - suspect that it's caching
        something internally which then doesn't get cleaned
        up properly...
        """
        current_widget = self
        while current_widget:
            if isinstance(current_widget, QtGui.QDialog):
                return current_widget

            current_widget = current_widget.parentWidget()

        return None

    def _on_screenshot(self):
        """
        Perform the actual screenshot
        """

        # hide the containing window - we can't actuall hide
        # the window as this will break modality!  Instead
        # we have to move the window off the screen:
        win = self._safe_get_dialog()

        win_geom = None
        if win:
            win_geom = win.geometry()
            win.setGeometry(1000000, 1000000, win_geom.width(),
                            win_geom.height())

            # make sure this event is processed:
            QtCore.QCoreApplication.processEvents()
            QtCore.QCoreApplication.sendPostedEvents(None, 0)
            QtCore.QCoreApplication.flush()

        try:
            # get temporary file to use:
            # to be cross-platform and python 2.5 compliant, we can't use
            # tempfile.NamedTemporaryFile with delete=False.  Instead, we
            # use tempfile.mkstemp which does practically the same thing!
            tf, path = tempfile.mkstemp(suffix=".png", prefix="tanktmp")
            if tf:
                os.close(tf)

            pm = screen_grab.screen_capture()

        finally:
            # restore the window:
            if win:
                win.setGeometry(win_geom)
                QtCore.QCoreApplication.processEvents()

        return pm
Ejemplo n.º 14
0
class FileListView(browser_widget.BrowserWidget):
    
    # signals - note, 'object' is used to avoid 
    # issues with PyQt when None is passed as PyQt 
    # doesn't allow None to be passed to classes 
    # other than object (an exception is raised)
    open_previous_workfile = QtCore.Signal(object)#FileItem
    open_previous_publish = QtCore.Signal(object)#FileItem
    view_in_shotgun = QtCore.Signal(object)#FileItem
    
    NO_TASK_NAME = "No Task"
    
    def __init__(self, parent=None):
        """
        Construction
        """
        browser_widget.BrowserWidget.__init__(self, parent)
        
        self._current_filter = {}
        
        # tweak style
        self.title_style = "none"
        
        self._update_title()  
    
    @property
    def selected_published_file(self):
        selected_item = self.get_selected_item()
        if selected_item:
            return selected_item.published_file
        return None

    @property
    def selected_work_file(self):
        selected_item = self.get_selected_item()
        if selected_item:
            return selected_item.work_file
        return None
    
    # Enable to force all work to be done in the main thread
    # which can help when debugging
    # IMPORTANT - set this to False before releasing!!!
    DEBUG_GET_DATA_IN_MAIN_THREAD=False
    
    def get_data(self, data):
        """
        Called by browser widget in worker thread to query the list
        of files to display for the specified context
        """
        if FileListView.DEBUG_GET_DATA_IN_MAIN_THREAD:
            # debug only - _get_data will be called first in
            # process_result which runs in the main thread
            return data
        else:
            return self._get_data(data)
        
    def _get_data(self, data):
        """
        Retrieve the list of files to display as well as the various display and grouping options required
        to build the file list.
        
        :param data:    Dictionary containing:
                        handler - A 'WorkFiles' instance containing the main application business logic
                        filter - The current 'FileFilter' instance being applied to the list
                        
        :returns:       Dictionary containing the various display & grouping options required to build the
                        file list as well as the list of files organised by task.
                        
        """
        result = {"task_groups":{}, "task_name_order":{}}
        
        handler = data["handler"]
        filter = data.get("filter")
        mode = filter.mode
        
        # get some additional info from the handler:
        ctx = handler.get_current_work_area()
        
        result["can_do_new_file"] = handler.can_do_new_file()
        result["have_valid_workarea"] = (ctx and (ctx.entity or ctx.project))
        result["have_valid_configuration"] = handler.have_valid_configuration_for_work_area()
        result["current_task_name"] = ctx.task.get("name") if ctx and ctx.task else None
        result["can_change_work_area"] = handler.can_change_work_area()
        result["filter"] = filter
        result["task_order"] = []
        
        if result["have_valid_workarea"] and result["have_valid_configuration"]:
        
            # get the list of files from the handler:
            files = handler.find_files(filter)
            
            # re-pivot this list of files ready to display:
            # 
            # builds the following structure
            # { task_name : { (file)name : { "files" : { 1:file,2:file, ... }, "thumbnail" : path, ... } } }
                        
            task_groups = {}
            for file in files:
                # first level is task group
                task_name = file.task.get("name") if file.task else FileListView.NO_TASK_NAME
                task_group = task_groups.setdefault(task_name, dict())
                
                # next level is name:
                name_group = task_group.setdefault(file.name, dict())
                
                # finally, add file to files:
                file_versions = name_group.setdefault("files", dict())
                file_versions[file.version] = file
                
            # do some pre-processing of file groups:
            filtered_task_groups = {}
            
            task_modified_pairs = []
            task_name_order = {}
            for task, name_groups in task_groups.iteritems():
                name_modified_pairs = []
                
                filtered_name_groups = {}
                
                for name, details in name_groups.iteritems():
                    files_versions = details["files"]
                    
                    # find highest version info:
                    local_versions = [f.version for f in files_versions.values() if f.is_local]
                    if mode == FileFilter.WORKFILES_MODE and not local_versions:
                        # don't have a version of this file to display!
                        continue
                    
                    publish_versions = [f.version for f in files_versions.values() if f.is_published]
                    if mode == FileFilter.PUBLISHES_MODE and not publish_versions:
                        # don't have a version of this file to display!
                        continue
                    
                    highest_local_version = -1
                    if local_versions:
                        highest_local_version = max(local_versions)
                        details["highest_local_file"] = files_versions[highest_local_version]
                        
                    highest_publish_version = -1
                    if publish_versions:
                        highest_publish_version = max(publish_versions)
                        details["highest_publish_file"] = files_versions[highest_publish_version]
                    
                    # find thumbnail to use:
                    sorted_versions = sorted(files_versions.keys(), reverse=True)
                    thumbnail = None
                    for version in sorted_versions:
                        # skip any versions that are greater than the one we are looking for
                        # Note: we shouldn't choose a thumbnail for versions that aren't
                        # going to be displayed so filter these out
                        if ((mode == FileFilter.WORKFILES_MODE and version > highest_local_version)
                            or (mode == FileFilter.PUBLISHES_MODE and version > highest_publish_version)):
                            continue
                        thumbnail = files_versions[version].thumbnail
                        if thumbnail:
                            # special case - update the thumbnail!
                            if mode == FileFilter.WORKFILES_MODE and version < highest_local_version:
                                files_versions[highest_local_version].set_thumbnail(thumbnail)
                            break
                    details["thumbnail"] = thumbnail
                    
                    # update group with details:
                    filtered_name_groups[name] = details

                    # determine when this file was last updated (modified or published)
                    # this is used to sort the files in the list:
                    last_updated = None
                    if mode == FileFilter.WORKFILES_MODE and highest_local_version >= 0:
                        last_updated = files_versions[highest_local_version].modified_at
                    if highest_publish_version >= 0:
                        published_at = files_versions[highest_publish_version].published_at
                        last_updated = max(last_updated, published_at) if last_updated else published_at
                    
                    name_modified_pairs.append((name, last_updated))
    
                if not filtered_name_groups:
                    # everything in this group was filtered out!
                    continue
                
                filtered_task_groups[task] = filtered_name_groups
    
                # sort names in reverse order of modified date:
                name_modified_pairs.sort(key=itemgetter(1), reverse=True)
                task_name_order[task] = [n for (n, _) in name_modified_pairs]
                
                task_modified_pairs.append((task, max([m for (_, m) in name_modified_pairs])))
        
            # sort tasks in reverse order of modified date:
            task_modified_pairs.sort(key=itemgetter(1), reverse=True)
            task_order = [n for (n, _) in task_modified_pairs]
        
            result["task_groups"] = filtered_task_groups
            result["task_name_order"] = task_name_order
            result["task_order"] = task_order
        
        return result
    
    def process_result(self, result):
        """
        Process list of tasks retrieved by get_data on the main thread
        
        :param result:  Dictionary containing the various display & grouping options required to build the
                        file list as well as the list of files organised by task.        
        """
        if FileListView.DEBUG_GET_DATA_IN_MAIN_THREAD:
            # gathering of data was not done in the get_data stage so we
            # should do it here instead - this method gets called in the 
            # main thread
            result = self._get_data(result)
        
        task_groups = result["task_groups"]
        task_name_order = result["task_name_order"]
        task_order = result["task_order"]
        current_task_name = result["current_task_name"]
        self._current_filter = result["filter"]
        
        self._update_title()
        
        if not task_groups:
            # build a useful error message using the info we have available:
            msg = ""
            if not result["can_change_work_area"]:
                if not result["have_valid_workarea"]:
                    msg = "The current Work Area is not valid!"
                elif not result["have_valid_configuration"]:
                    msg = ("Shotgun File Manager has not been configured for the environment "
                           "being used by the selected Work Area!")
                elif not result["can_do_new_file"]:
                    msg = "Couldn't find any files in this Work Area!"
                else:
                    msg = "Couldn't find any files!\nClick the New file button to start work."
            else:
                if not result["have_valid_workarea"]:
                    msg = "The current Work Area is not valid!"
                elif not result["have_valid_configuration"]:
                    msg = ("Shotgun File Manager has not been configured for the environment "
                           "being used by the selected Work Area!\n"
                           "Please choose a different Work Area to continue.")
                elif not result["can_do_new_file"]:
                    msg = "Couldn't find any files in this Work Area!\nTry selecting a different Work Area."
                else:
                    msg = "Couldn't find any files!\nClick the New file button to start work."
            self.set_message(msg)
            return
        
        for task_name in task_order:
            name_groups = task_groups[task_name]
        
            if (len(task_groups) > 1 
                or (task_name != current_task_name
                    and task_name != FileListView.NO_TASK_NAME 
                    and current_task_name == None)):
                # add header for task:
                h = self.add_item(browser_widget.ListHeader)
                h.set_title("%s" % (task_name))
            
            ordered_names = task_name_order[task_name]
            for name in ordered_names:
                details = name_groups[name]
                
                files = details["files"]
                highest_local_file = details.get("highest_local_file")
                highest_publish_file = details.get("highest_publish_file")
                thumbnail = details["thumbnail"]
                
                # add new item to list:
                item = self._add_file_item(highest_publish_file, highest_local_file)
                if not item:
                    continue
                
                # set thumbnail if have one:
                if thumbnail:
                    item.set_thumbnail(thumbnail)
                
                # add context menu:
                item.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)

                # if it's a publish then add 'View In Shotgun' item:
                if highest_publish_file:
                    action = QtGui.QAction("View latest Publish in Shotgun", item)
                    # (AD) - the '[()]' syntax in action.triggered[()].connect looks weird right!
                    # 'triggered' is a QtCore.SignalInstance which actually defines multiple
                    # signals: triggered() & triggered(bool).  PySide will correctly determine which
                    # one to use but PyQt gets confused and calls the (bool) version instead which
                    # causes problems for us...  Luckily, Qt lets us use the argument list () to 
                    # index into the SignalInstance object to force the use of the non-bool version - yay!
                    action.triggered[()].connect(lambda f=highest_publish_file: self._on_show_in_shotgun_action_triggered(f))
                    item.addAction(action)

                # build context menu for all publish versions:                
                published_versions = [f.version for f in files.values() if f.is_published and isinstance(f.version, int)]
                if published_versions:
                    
                    published_versions.sort(reverse=True)
                    
                    publishes_action = QtGui.QAction("Open Publish Read-Only", item)
                    publishes_sm = QtGui.QMenu(item)
                    publishes_action.setMenu(publishes_sm)
                    item.addAction(publishes_action)    
                     
                    for v in published_versions[:20]:
                        f = files[v]
                        msg = ("v%03d" % f.version)
                        action = QtGui.QAction(msg, publishes_sm)
                        # see above for explanation of [()] syntax in action.triggered[()].connect...
                        action.triggered[()].connect(lambda f=f: self._on_open_publish_action_triggered(f))
                        publishes_sm.addAction(action)
                     
                # build context menu for all work files:
                wf_versions = [f.version for f in files.values() if f.is_local and isinstance(f.version, int)]
                if wf_versions:
                    
                    wf_versions.sort(reverse=True)
                    
                    wf_action = QtGui.QAction("Open Work File", item)
                    wf_sm = QtGui.QMenu(item)
                    wf_action.setMenu(wf_sm)
                    item.addAction(wf_action)    
                     
                    for v in wf_versions[:20]:
                        f = files[v]
                        msg = ("v%03d" % f.version)
                        action = QtGui.QAction(msg, wf_sm)
                        # see above for explanation of [()] syntax in action.triggered[()].connect...
                        action.triggered[()].connect(lambda f=f: self._on_open_workfile_action_triggered(f))
                        wf_sm.addAction(action)                

    def _update_title(self):
        """
        Update the list title depending on the mode
        """
        if not self._current_filter:
            return
        self.set_label(self._current_filter.list_title)
                               
    def _add_file_item(self, latest_published_file, latest_work_file):
        """
        Add an item to the file list given the latest publish & work files
        
        :param latest_published_file:    The latest published version of the file to be added
        :param latest_work_file:         The latest work/local version of the file to be added
        """
        details = ""
        tooltip = ""
                   
        # colours for item titles:
        red = "rgb(200, 84, 74)"
        green = "rgb(145, 206, 95)"
        
        current_mode = self._current_filter.mode
        
        file = None
        editable = True
        not_editable_reason = ""
        if current_mode == FileFilter.WORKFILES_MODE:
            file = latest_work_file
            
            title_colour = None
            if latest_published_file:
                if file.compare_with_publish(latest_published_file) >= 0:
                    # work file is most recent
                    title_colour = green
                    tooltip += "This is the latest version of this file"
                else:
                    # published file is most recent
                    title_colour = red
                    tooltip += "<b>A more recent published version of this file is available:</b>"
                    tooltip += "<br>"
                    tooltip += ("<br><b>Version v%03d</b>" % latest_published_file.version)
                    tooltip += "<br>" + latest_published_file.format_published_by_details()
                    tooltip += "<br>"
                    tooltip += "<br><b>Description:</b>"
                    tooltip += "<br>" + latest_published_file.format_publish_description()
            else:
                tooltip += "This file has never been published"

            if file.version is not None:
                details = "<b>%s, v%03d</b>" % (file.name, file.version)
            else:
                details = "<b>%s</b>" % (file.name)
            if title_colour:    
                details = "<span style='color:%s'>%s</span>" % (title_colour, details)
            details += "<br>" + file.format_modified_by_details()
            
            editable = file.editable
            not_editable_reason = file.not_editable_reason
                
        elif current_mode == FileFilter.PUBLISHES_MODE:
            file = latest_published_file
            
            title_colour = None
            tooltip += "<b>Description:</b>"
            tooltip += "<br>" + file.format_publish_description()
            
            tooltip += "<hr>"
            if latest_work_file:
                if latest_work_file.compare_with_publish(file) <= 0:
                    # published file is most recent
                    title_colour = green
                    tooltip += "This is the latest version of this file"
                else:
                    # work file is most recent
                    #title_colour = red
                    tooltip += "<b>A more recent version of this file was found in your work area:</b>"
                    tooltip += "<br>"
                    #tooltip += "<br><b>Details:</b>"
                    tooltip += ("<br><b>Version v%03d</b>" % latest_work_file.version)
                    
                    tooltip += "<br>" + latest_work_file.format_modified_by_details()
            else:
                title_colour = green
                tooltip += "This is the latest version of this file"
            
            details = "<b>%s, v%03d</b>" % (file.name, file.version)
            if title_colour:    
                details = "<span style='color:%s'>%s</span>" % (title_colour, details)
            
            details += "<br>" + file.format_published_by_details()
            
            editable = file.editable
            not_editable_reason = file.not_editable_reason
        else:
            raise TankError("Display mode is not recognised!")
            
        # update editable info on the tooltip
        if not editable:
            tooltip += "<hr>"   
            tooltip += "Read-only: " + not_editable_reason
        
        # add item:
        item = self.add_item(FileItemForm)
        item.published_file = latest_published_file
        item.work_file = latest_work_file

        # set tool tip
        item.setToolTip(tooltip)
        
        # build and set details string:
        item.set_details(details)
        
        item.set_is_editable(editable, not_editable_reason)
        
        return item
                   
    def _on_open_workfile_action_triggered(self, file):
        """
        Open action triggered from context menu
        """
        self.open_previous_workfile.emit(file)

    def _on_open_publish_action_triggered(self, file):
        """
        Open action triggered from context menu
        """
        self.open_previous_publish.emit(file)

    def _on_show_in_shotgun_action_triggered(self, file):
        """
        Show in Shotgun action triggered from context menu
        """
        self.view_in_shotgun.emit(file)
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
Ejemplo n.º 15
0
class ThumbnailWidget(QtGui.QWidget):
    """
    Thumbnail widget that provides screen capture functionality
    """

    thumbnail_changed = QtCore.Signal()

    def __init__(self, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)

        self._ui = Ui_ThumbnailWidget()
        self._ui.setupUi(self)

        # create layout to control buttons frame
        layout = QtGui.QHBoxLayout()
        layout.addWidget(self._ui.buttons_frame)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        self.setLayout(layout)

        # connect to buttons:
        self._ui.camera_btn.clicked.connect(self._on_camera_clicked)

        self._btns_transition_anim = None
        self._update_ui()

    @property
    def thumbnail(self):
        pm = self._ui.thumbnail.pixmap()
        return pm if pm and not pm.isNull() else None

    @thumbnail.setter
    def thumbnail(self, value):
        self._ui.thumbnail.setPixmap(value if value else QtGui.QPixmap())
        self._update_ui()
        self.thumbnail_changed.emit()

    def enable_screen_capture(self, enable):
        self._ui.camera_btn.setVisible(enable)

    def resizeEvent(self, event):
        self._update_ui()

    def enterEvent(self, event):
        """
        when the cursor enters the control, show the buttons
        """
        if self.thumbnail and self._are_any_btns_enabled():
            self._ui.buttons_frame.show()
            self._run_btns_transition_anim(QtCore.QAbstractAnimation.Forward)

    def leaveEvent(self, event):
        """
        when the cursor leaves the control, hide the buttons
        """
        if self.thumbnail and self._are_any_btns_enabled():
            self._run_btns_transition_anim(QtCore.QAbstractAnimation.Backward)

    def _are_any_btns_enabled(self):
        """
        Return if any of the buttons are enabled
        """
        return not (self._ui.camera_btn.isHidden())

    """
    button visibility property used by QPropertyAnimation
    """

    def get_btn_visibility(self):
        return self._btns_visibility

    def set_btn_visibility(self, value):
        self._btns_visibility = value
        self._ui.buttons_frame.setStyleSheet(
            "#buttons_frame {border-radius: 2px; background-color: rgba(32, 32, 32, %d);}"
            % (64 * value))

    btn_visibility = QtCore.Property(float, get_btn_visibility,
                                     set_btn_visibility)

    def _run_btns_transition_anim(self, direction):
        """
        Run the transition animation for the buttons
        """
        if not self._btns_transition_anim:
            # set up anim:
            self._btns_transition_anim = QtCore.QPropertyAnimation(
                self, "btn_visibility")
            self._btns_transition_anim.setDuration(150)
            self._btns_transition_anim.setStartValue(0.0)
            self._btns_transition_anim.setEndValue(1.0)
            self._btns_transition_anim.finished.connect(
                self._on_btns_transition_anim_finished)

        if self._btns_transition_anim.state(
        ) == QtCore.QAbstractAnimation.Running:
            if self._btns_transition_anim.direction() != direction:
                self._btns_transition_anim.pause()
                self._btns_transition_anim.setDirection(direction)
                self._btns_transition_anim.resume()
            else:
                pass  # just let animation continue!
        else:
            self._btns_transition_anim.setDirection(direction)
            self._btns_transition_anim.start()

    def _on_btns_transition_anim_finished(self):
        if self._btns_transition_anim.direction(
        ) == QtCore.QAbstractAnimation.Backward:
            self._ui.buttons_frame.hide()

    def _on_camera_clicked(self):
        pm = self._on_screenshot()
        if pm:
            self.thumbnail = pm

    def _update_ui(self):

        # maximum size of thumbnail is widget geom:
        thumbnail_geom = self.geometry()
        thumbnail_geom.moveTo(0, 0)
        scale_contents = False

        pm = self.thumbnail
        if pm:
            # work out size thumbnail should be to maximize size
            # whilst retaining aspect ratio
            pm_sz = pm.size()

            h_scale = float(thumbnail_geom.height() - 4) / float(
                pm_sz.height())
            w_scale = float(thumbnail_geom.width() - 4) / float(pm_sz.width())
            scale = min(1.0, h_scale, w_scale)
            scale_contents = (scale < 1.0)

            new_height = min(int(pm_sz.height() * scale),
                             thumbnail_geom.height())
            new_width = min(int(pm_sz.width() * scale), thumbnail_geom.width())

            new_geom = QtCore.QRect(thumbnail_geom)
            new_geom.moveLeft((
                (thumbnail_geom.width() - 4) / 2 - new_width / 2) + 2)
            new_geom.moveTop((
                (thumbnail_geom.height() - 4) / 2 - new_height / 2) + 2)
            new_geom.setWidth(new_width)
            new_geom.setHeight(new_height)
            thumbnail_geom = new_geom

        self._ui.thumbnail.setScaledContents(scale_contents)
        self._ui.thumbnail.setGeometry(thumbnail_geom)

        # now update buttons based on current thumbnail:
        if not self._btns_transition_anim or self._btns_transition_anim.state(
        ) == QtCore.QAbstractAnimation.Stopped:
            if self.thumbnail or not self._are_any_btns_enabled():
                self._ui.buttons_frame.hide()
                self._btns_visibility = 0.0
            else:
                self._ui.buttons_frame.show()
                self._btns_visibility = 1.0

    def _safe_get_dialog(self):
        """
        Get the widgets dialog parent.  
        
        just call self.window() but this is unstable in Nuke
        Previously this would
        causing a crash on exit - suspect that it's caching
        something internally which then doesn't get cleaned
        up properly...
        """
        current_widget = self
        while current_widget:
            if isinstance(current_widget, QtGui.QDialog):
                return current_widget

            current_widget = current_widget.parentWidget()

        return None

    class ScreenshotThread(QtCore.QThread):
        """
        Wrap screenshot call in a thread just to be on the safe side!  
        This helps avoid the os thinking the application has hung for 
        certain applications (e.g. Softimage on Windows)
        """
        def __init__(self, path):
            QtCore.QThread.__init__(self)
            self._path = path
            self._error = None

        def get_error(self):
            return self._error

        def run(self):
            try:
                if sys.platform == "darwin":
                    # use built-in screenshot command on the mac
                    os.system("screencapture -m -i -s %s" % self._path)
                elif sys.platform == "linux2":
                    # use image magick
                    os.system("import %s" % self._path)
                elif sys.platform == "win32":
                    # use external boxcutter tool
                    bc = os.path.abspath(
                        os.path.join(__file__, "../resources/boxcutter.exe"))
                    subprocess.check_call([bc, self._path])
            except Exception, e:
                self._error = str(e)
Ejemplo n.º 16
0
class SgProjectModel(ShotgunModel):
    """
    This model represents the data which is displayed in the projects list view
    """

    DISPLAY_NAME_ROLE = QtCore.Qt.UserRole + 101

    thumbnail_updated = QtCore.Signal(QtGui.QStandardItem)
    project_launched = QtCore.Signal()

    _supports_project_templates = None

    @classmethod
    def supports_project_templates(cls):
        """
        Tests if Shotgun 6.0 Project Templates are supported on the server. If
        this method has never been called, the server will be contacted
        synchronously and the result will be cached so subsequent calls are
        faster.

        :returns: True if the server supports Shotgun 6.0 Project Templates,
                  False otherwise.
        """

        connection = sgtk.platform.current_engine().shotgun

        # If we haven't checked on the server yet.
        if cls._supports_project_templates is None:
            try:
                # Try to read the field's schema.
                connection.schema_field_read("Project", "is_template")
                # It worked therefore it exists.
                cls._supports_project_templates = True
            except Exception:
                # We got an error, so it doesn't exist.
                cls._supports_project_templates = False
        return cls._supports_project_templates

    def __init__(self, parent, overlay_parent_widget):
        """ Constructor """
        ShotgunModel.__init__(self, parent, download_thumbs=True)

        # load up the thumbnail to use when there is none set in Shotgun
        self._missing_thumbnail_project = QtGui.QPixmap(
            ":/tk-desktop/missing_thumbnail_project.png"
        )

        # load up the cached data for the model
        filters = [["name", "is_not", "Template Project"], ["archived", "is_not", True]]
        # Template projects is a Shotgun 6.0 feature, so make sure it exists
        # on the server before filtering on that value.
        if SgProjectModel.supports_project_templates():
            filters.append(["is_template", "is_not", True])

        interesting_fields = [
            "name",
            "sg_status",
            "current_user_favorite",
            "last_accessed_by_current_user",
            "sg_description",
        ]
        ShotgunModel._load_data(
            self,
            entity_type="Project",
            filters=filters,
            hierarchy=["name"],
            fields=interesting_fields,
            order=[],
        )

        # and force a refresh of the data from Shotgun
        self._refresh_data()

    def update_project_accessed_time(self, project):
        """
        Set the current user's last-accessed time for the given project.

        This will update the value in the model and in Shotgun.
        """
        if project is None:
            return

        # Update Project.last_accessed_by_current_user in Shotgun
        engine = sgtk.platform.current_engine()
        engine.shotgun.update_project_last_accessed(project, engine.get_current_login())

        # Update the data in the model
        item = self.item_from_entity("Project", project["id"])
        # set to unix seconds rather than datetime to be compatible with Shotgun model
        utc_now_epoch = time.mktime(datetime.datetime.utcnow().utctimetuple())
        project["last_accessed_by_current_user"] = utc_now_epoch
        item.setData(project, ShotgunModel.SG_DATA_ROLE)

        self.project_launched.emit()

    def _populate_item(self, item, sg_data):
        """
        Implement the abstract class method from ShotgunModel.

        Set the item data based on the Shotgun project.
        """
        item.setData(sg_data.get("name", "No Name"), self.DISPLAY_NAME_ROLE)

    def _populate_default_thumbnail(self, item):
        """
        Implement the abstract class method from ShotgunModel.

        Set the default thumbnail to the missing thumbnail.
        """
        item.setIcon(self._missing_thumbnail_project)

    def _populate_thumbnail(self, item, field, path):
        """
        Implement the abstract class method from ShotgunModel.

        Set the thumbnail directly from the path.
        """
        # first load as a pixmap to avoid the icon delayed loading
        thumb = QtGui.QPixmap(path)
        item.setIcon(thumb)

        # signal anybody listening for thumbnail updates
        self.thumbnail_updated.emit(item)
Ejemplo n.º 17
0
class WorkFilesForm(QtGui.QWidget):
    """
    Primary work area UI
    """

    # signals - note, 'object' is used to avoid
    # issues with PyQt when None is passed
    open_publish = QtCore.Signal(object, object,
                                 bool)  #FileItem, FileItem, bool
    open_workfile = QtCore.Signal(object, object,
                                  bool)  #FileItem, FileItem, bool
    open_previous_publish = QtCore.Signal(object)  #FileItem
    open_previous_workfile = QtCore.Signal(object)  #FileItem

    new_file = QtCore.Signal()

    show_in_fs = QtCore.Signal()
    show_in_shotgun = QtCore.Signal(object)  #FileItem

    def __init__(self, app, handler, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)

        self._app = app
        self._handler = handler

        # set up the UI
        from .ui.work_files_form import Ui_WorkFilesForm
        self._ui = Ui_WorkFilesForm()
        self._ui.setupUi(self)

        # patch up the lines to be the same colour as the font:
        clr = QtGui.QApplication.palette().color(QtGui.QPalette.Text)
        rgb_str = "rgb(%d,%d,%d)" % (clr.red(), clr.green(), clr.blue())
        self._ui.project_line.setStyleSheet(
            "#project_line{background-color: %s;}" % rgb_str)
        self._ui.entity_line.setStyleSheet(
            "#entity_line{background-color: %s;}" % rgb_str)
        self._ui.task_line.setStyleSheet("#task_line{background-color: %s;}" %
                                         rgb_str)

        ss = "{font-size: 14pt; border-style: dashed; border-width: 2px; border-radius: 3px; border-color: %s;}" % rgb_str
        self._ui.no_project_label.setStyleSheet("#no_project_label %s" % ss)
        self._ui.no_task_label.setStyleSheet("#no_task_label %s" % ss)
        self._ui.no_entity_label.setStyleSheet("#no_entity_label %s" % ss)

        # set up the work area depending on if it's possible to change it:
        if self._handler.can_change_work_area():
            # update the style sheet for the work area form so it
            # becomes highlighted when hovering over it
            # test for lightness ahead of use to stay compatible with Qt 4.6.2
            if hasattr(clr, 'lightness'):
                clr = QtGui.QApplication.palette().color(QtGui.QPalette.Window)
                clr = clr.lighter() if clr.lightness() < 128 else clr.darker()
                ss = self._ui.work_area_frame.styleSheet()
                ss = "%s #work_area_frame:hover {background-color: rgb(%d,%d,%d);}" % (
                    ss, clr.red(), clr.green(), clr.blue())
                self._ui.work_area_frame.setStyleSheet(ss)

            self._ui.work_area_frame.setCursor(QtCore.Qt.PointingHandCursor)
            self._ui.work_area_frame.setToolTip("Click to Change Work Area...")
            self._ui.no_change_frame.setVisible(False)

            self._ui.work_area_frame.mousePressEvent = self._on_work_area_mouse_press_event
        else:
            self._ui.work_area_frame.setCursor(QtCore.Qt.ArrowCursor)
            self._ui.work_area_frame.setToolTip(
                "The Work Area is locked and cannot be changed")
            self._ui.no_change_frame.setVisible(True)

        # connect up controls:
        self._ui.show_in_fs_label.mousePressEvent = self._on_show_in_fs_mouse_press_event

        self._ui.filter_combo.currentIndexChanged.connect(
            self._on_filter_selection_changed)

        self._ui.open_file_btn.clicked.connect(self._on_open_file)
        self._ui.new_file_btn.clicked.connect(self._on_new_file)

        self._ui.file_list.action_requested.connect(self._on_open_file)
        self._ui.file_list.selection_changed.connect(
            self._on_file_selection_changed)

        self._ui.file_list.set_app(self._app)
        self._ui.file_list.open_previous_workfile.connect(
            self._on_open_previous_workfile)
        self._ui.file_list.open_previous_publish.connect(
            self._on_open_previous_publish)
        self._ui.file_list.view_in_shotgun.connect(self._on_view_in_shotgun)

        # set up the work area:
        ctx = self._handler.get_current_work_area()
        self._set_work_area(ctx)

        self._on_file_selection_changed()

    @property
    def filter(self):
        return self._get_current_filter()

    def _on_view_in_shotgun(self, file):
        self.show_in_shotgun.emit(file)

    def _on_show_in_fs_mouse_press_event(self, event):
        """
        Emit event when the user clicks the show in file system link:
        """
        self.show_in_fs.emit()

    def closeEvent(self, e):
        """
        Ensure everything is cleaned up when
        the widget is closed
        """
        self._ui.file_list.destroy()
        return QtGui.QWidget.closeEvent(self, e)

    def _on_open_file(self):
        """
        """
        # get the currently selected work file

        work_file = self._ui.file_list.selected_work_file
        published_file = self._ui.file_list.selected_published_file

        current_filter = self._get_current_filter()
        if not current_filter:
            return

        if current_filter.mode == FileFilter.WORKFILES_MODE:
            self.open_workfile.emit(work_file, published_file, False)
        else:  # current_filter.mode == FileFilter.PUBLISHES_MODE:
            self.open_publish.emit(published_file, work_file, False)

    def _on_open_previous_workfile(self, file):
        """
        """
        self.open_previous_workfile.emit(file)

    def _on_open_previous_publish(self, file):
        """
        """
        self.open_previous_publish.emit(file)

    def _on_new_file(self):
        """
        """
        self.new_file.emit()

    def _set_work_area(self, ctx):
        """
        Set the current work area to the specified context.
        """

        self._app.log_debug(
            "Setting the work area in the File Manager UI")  # to %s..." % ctx)

        # update work area info:
        self._app.log_debug(" - Updating Work Area")
        self._update_work_area(ctx)

        # update the filter menu:
        self._app.log_debug(" - Updating Filter menu")
        self._update_filter_menu()

        # finally, update file list:
        self._app.log_debug(" - Refreshing File List")
        self._refresh_file_list()

        # update new button enabled state
        can_do_new = self._handler.can_do_new_file()
        self._ui.new_file_btn.setEnabled(can_do_new)

        self._app.log_debug(
            "Finished setting the work area in the File Manager UI!")

    def _on_work_area_mouse_press_event(self, event):
        """
        Event triggered when mouse is pressed in the work area
        form
        """
        new_ctx = self._handler.select_work_area()
        if new_ctx:
            self._set_work_area(new_ctx[0])

    def _refresh_file_list(self):
        """
        Refresh the file list based on the current filter
        """
        # get the file filter:
        filter = self._get_current_filter()
        if not filter:
            return

        # hide/show the show-in-filesystem link:
        self._ui.show_in_fs_label.setVisible(filter.show_in_file_system)

        # clear and reload list:
        self._ui.file_list.clear()
        self._ui.file_list.load({"handler": self._handler, "filter": filter})
        self._on_file_selection_changed()

    def _on_filter_selection_changed(self, idx):
        """
        Called when the filter is changed
        """
        self._refresh_file_list()

    def _on_file_selection_changed(self):
        """
        
        """
        something_selected = (
            self._ui.file_list.selected_published_file is not None
            or self._ui.file_list.selected_work_file is not None)
        self._ui.open_file_btn.setEnabled(something_selected)

    def _update_filter_menu(self):
        """
        Update the list of users to display work files for
        """
        # get list of filters from handler:
        filters = self._handler.get_file_filters()

        # get selected filter:
        previous_filter = self._get_current_filter()

        # clear menu:
        self._ui.filter_combo.clear()

        # add back in filters:
        selected_idx = 0
        separator_count = 0
        for filter in filters:

            if filter == "separator":
                # special 'filter' signifying a separator should be added
                # - don't add it yet though in case there aren't any more
                # non separator items following!
                separator_count += 1
                continue

            if not filter.menu_label:
                # filter doesn't have a menu label - bad!
                continue

            # add any separators:
            while separator_count > 0:
                separator_count -= 1
                self._ui.filter_combo.insertSeparator(
                    self._ui.filter_combo.count())

            # see if this is the previously selected filter:
            if filter == previous_filter:
                selected_idx = self._ui.filter_combo.count()

            # add filter:
            self._ui.filter_combo.addItem(filter.menu_label, filter)

        # set the current index:
        self._ui.filter_combo.setCurrentIndex(selected_idx)

    def _get_current_filter(self):
        """
        Get the current filter
        """
        filter = None
        idx = self._ui.filter_combo.currentIndex()

        if idx >= 0:
            filter = self._ui.filter_combo.itemData(idx)

            # convert from QVariant object if itemData is returned as such
            if hasattr(QtCore, "QVariant") and isinstance(
                    filter, QtCore.QVariant):
                filter = filter.toPyObject()

        return filter

    def _update_work_area(self, ctx):
        """
        A lot of this should be done in a worker thread!
        """
        if ctx and ctx.project:
            # context has a project - that's something at least!
            self._ui.project_pages.setCurrentWidget(self._ui.project_page)

            # get additional details:
            sg_details = {}
            try:
                sg_details = self._app.shotgun.find_one(
                    "Project", [["id", "is", ctx.project["id"]]],
                    ["sg_description", "image", "code"])
            except:
                pass

            # header:
            project_name = ctx.project.get("name") or sg_details.get("code")
            self._ui.project_label.setText("Project: %s" %
                                           (project_name or "-"))
            self._ui.project_frame.setToolTip("%s" % (project_name or ""))

            # thumbnail:
            project_thumbnail = QtGui.QPixmap()
            project_img_url = sg_details.get("image")
            if project_img_url:
                thumbnail_path = self._download_thumbnail(project_img_url)
                if thumbnail_path:
                    project_thumbnail = QtGui.QPixmap(thumbnail_path)
            self._set_thumbnail(self._ui.project_thumbnail, project_thumbnail)

            # description:
            desc = sg_details.get(
                "sg_description"
            ) or "<i>No description was entered for this Project</i>"
            self._ui.project_description.setText(desc)

            if ctx.entity:
                # work area defined - yay!
                self._ui.entity_pages.setCurrentWidget(self._ui.entity_page)
                self._ui.entity_pages.setVisible(True)

                # get any extra fields that have been defined for this entity type.  This will be a dictionary
                # of label:field pairs for the current entity type:
                extra_fields = self._app.get_setting(
                    "sg_entity_type_extra_display_fields",
                    {}).get(ctx.entity["type"], {})

                # get additional details:
                sg_details = {}
                try:
                    sg_details = self._app.shotgun.find_one(
                        ctx.entity["type"], [["project", "is", ctx.project],
                                             ["id", "is", ctx.entity["id"]]],
                        ["description", "image", "code"] +
                        extra_fields.values())
                except:
                    pass

                # header:
                entity_type_name = tank.util.get_entity_type_display_name(
                    self._app.tank, ctx.entity.get("type"))
                entity_name = ctx.entity.get("name") or sg_details.get("code")
                self._ui.entity_label.setText(
                    "%s: %s" % (entity_type_name, entity_name or "-"))
                self._ui.entity_frame.setToolTip("%s" % (entity_name or ""))

                # thumbnail:
                entity_thumbnail = QtGui.QPixmap()
                entity_img_url = sg_details.get("image")
                if entity_img_url:
                    thumbnail_path = self._download_thumbnail(entity_img_url)
                    if thumbnail_path:
                        entity_thumbnail = QtGui.QPixmap(thumbnail_path)
                self._set_thumbnail(self._ui.entity_thumbnail,
                                    entity_thumbnail)

                # description including the display of extra fields:
                extra_info = ", ".join([
                    "%s: %s" % (label, str(sg_details.get(field)))
                    for label, field in extra_fields.iteritems()
                ])
                desc = ""
                if extra_info:
                    desc += "(%s)<br>" % extra_info
                desc += sg_details.get("description") or (
                    "<i>No description was entered for this %s</i>" %
                    entity_type_name)
                self._ui.entity_description.setText(desc)

                # task:
                if ctx.task:
                    # have a task - double yay!!
                    self._ui.task_pages.setCurrentWidget(self._ui.task_page)
                    self._ui.task_pages.setVisible(True)

                    # get additional details:
                    sg_details = {}
                    try:
                        sg_details = self._app.shotgun.find_one(
                            "Task", [["project", "is", ctx.project],
                                     ["id", "is", ctx.task["id"]]],
                            ["task_assignees", "step.Step.code", "content"])
                    except Exception, e:
                        pass

                    # header:
                    task_name = ctx.task.get("name") or sg_details.get(
                        "content")
                    self._ui.task_label.setText("Task: %s" %
                                                (task_name or "-"))
                    self._ui.task_frame.setToolTip("%s" % (task_name or ""))

                    # thumbnail:
                    task_thumbnail = QtGui.QPixmap()
                    task_assignees = sg_details.get("task_assignees", [])
                    if len(task_assignees) > 0:
                        user_id = task_assignees[0]["id"]

                        sg_user_details = {}
                        try:
                            sg_user_details = self._app.shotgun.find_one(
                                "HumanUser", [["id", "is", user_id]],
                                ["image"])
                        except Exception, e:
                            pass

                        if sg_user_details:
                            img_url = sg_user_details.get("image")
                            if img_url:
                                thumbnail_path = self._download_thumbnail(
                                    img_url)
                                if thumbnail_path:
                                    task_thumbnail = QtGui.QPixmap(
                                        thumbnail_path)

                    self._set_thumbnail(self._ui.task_thumbnail,
                                        task_thumbnail)

                    # details
                    assignees = []
                    for assignee in sg_details.get("task_assignees", []):
                        name = assignee.get("name")
                        if name:
                            assignees.append(name)
                    assignees_str = ", ".join(assignees) if assignees else "-"
                    step_str = sg_details.get("step.Step.code")
                    if step_str is None:
                        step_str = "-"
                    self._ui.task_description.setText(
                        "Assigned to: %s<br>Pipeline Step: %s" %
                        (assignees_str, step_str))

                else:
                    # task not chosen
                    if not self._handler.can_change_work_area():
                        self._ui.task_pages.setVisible(False)
                    else:
                        self._ui.task_pages.setCurrentWidget(
                            self._ui.no_task_page)
            else:
Ejemplo n.º 18
0
class PublishResultForm(QtGui.QWidget):
    """
    Implementation of the main publish UI
    """

    close = QtCore.Signal()

    def __init__(self, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)

        self._status = True
        self._errors = []

        # set up the UI
        from .ui.publish_result_form import Ui_PublishResultForm
        self._ui = Ui_PublishResultForm()
        self._ui.setupUi(self)

        self._ui.close_btn.clicked.connect(self._on_close)

        self._update_ui()

    # @property
    def __get_status(self):
        return self._status

    # @status.setter
    def __set_status(self, value):
        self._status = value
        self._update_ui()

    status = property(__get_status, __set_status)

    # @property
    def __get_errors(self):
        return self._errors

    # @errors.setter
    def __set_errors(self, value):
        self._errors = value
        self._update_ui()

    errors = property(__get_errors, __set_errors)

    def _on_close(self):
        self.close.emit()

    def _update_ui(self):
        self._ui.status_icon.setPixmap(
            QtGui.QPixmap([":/res/failure.png",
                           ":/res/success.png"][self._status]))
        self._ui.status_title.setText(["Failure!", "Success"][self._status])

        details = ""
        if self._status:
            details = ("Your Publish has  successfully completed. Your "
                       "work has been shared, your scene has been "
                       "versioned up and your mates have been notified!")
        else:
            details = "\n\n".join(self._errors)
        self._ui.status_details.setText(details)
Ejemplo n.º 19
0
class GroupingModel(QtGui.QStandardItemModel):
    """ A model that manages items in collapsible groups. """
    GROUP_ROLE = QtCore.Qt.UserRole + 1001  # stores the group an item is in
    GROUP_RANK_ROLE = QtCore.Qt.UserRole + 1002  # stores the ordering for a group
    GROUP_VISIBLE_ROLE = QtCore.Qt.UserRole + 1003  # store whether a group is collapsed
    ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 1004  # store the type of an item (header, footer, content)

    # the types for non-content items
    ITEM_TYPE_HEADER = "group_header"
    ITEM_TYPE_FOOTER = "group_footer"

    # a signal that is emitted when a group is expanded or collapsed
    group_toggled = QtCore.Signal(str, bool)

    # a signal that is emitted when groups are modified
    groups_modified = QtCore.Signal()

    def __init__(self, parent=None):
        QtGui.QStandardItemModel.__init__(self, parent)

        # key for the default group
        self.__groups = {}
        self.__default_group = None

        # keep track of changes that could effect grouping
        self.rowsRemoved.connect(self.__handle_rows_removed)

    def clear(self):
        # clear groups and default group in addition to the model
        QtGui.QStandardItemModel.clear(self)
        if self.__groups:
            self.__groups = {}
            self.__default_group = None
            self.groups_modified.emit()

    # manage groups
    def create_group(self, group_key, expanded=True):
        """
        Create a group for group_key

        Returns a tuple of the header and footer model items created for the group.
        """
        if group_key in self.__groups:
            raise ValueError("group already exists '%s'" % group_key)

        # create the header item, defaults to expanded
        header = QtGui.QStandardItem()
        header.setData(self.ITEM_TYPE_HEADER, self.ITEM_TYPE_ROLE)
        header.setData(group_key, self.GROUP_ROLE)
        header.setData(expanded, self.GROUP_VISIBLE_ROLE)

        # create the footer item
        footer = QtGui.QStandardItem()
        footer.setData(self.ITEM_TYPE_FOOTER, self.ITEM_TYPE_ROLE)
        footer.setData(group_key, self.GROUP_ROLE)

        # set flags so clicks flow through to these items
        header.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
        footer.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)

        # track the header and the footer
        self.__groups[group_key] = {
            "header": header,
            "footer": footer,
        }

        # and add to the model
        self.appendRow(header)
        self.appendRow(footer)

        self.groups_modified.emit()
        return (header, footer)

    def set_default_group(self, group_key):
        """ Set the group that an item if there is no explicit assignment """
        if group_key not in self.__groups:
            raise KeyError("no group '%s'" % group_key)
        if self.__default_group != group_key:
            self.groups_modified.emit()
            self.__default_group = group_key

    def get_header_items(self):
        """ Returns all the header items """
        self.__get_items_of_type(self.ITEM_TYPE_HEADER)

    def get_footer_items(self):
        """ Returns all the footer items """
        self.__get_items_of_type(self.ITEM_TYPE_FOOTER)

    def get_group_header(self, group_key):
        """ Returns the header item for the given group """
        if group_key not in self.__groups:
            raise KeyError("no group '%s'" % group_key)
        return self.__groups[group_key]["header"]

    def get_group_footer(self, group_key):
        """ Returns the footer item for the given group """
        if group_key not in self.__groups:
            raise KeyError("no group '%s'" % group_key)
        return self.__groups[group_key].get("footer")

    def get_items_in_group(self, group):
        """ Returns all the content items for the given group """
        start = self.index(0, 0)
        match_flags = QtCore.Qt.MatchExactly
        matching_indexes = self.match(start, self.GROUP_ROLE, group, -1,
                                      match_flags)
        return [
            index for index in matching_indexes
            if index.data(self.ITEM_TYPE_ROLE) is None
        ]

    def set_group_rank(self, group_key, rank):
        """ Set the rank for the given group.  Groups are ordered by rank. """
        # get all items with matching group_key
        start = self.index(0, 0)
        match_flags = QtCore.Qt.MatchExactly
        matching_indexes = self.match(start, self.GROUP_ROLE, group_key, -1,
                                      match_flags)

        if len(matching_indexes) == 0:
            raise KeyError("no group '%s'" % group_key)

        # and update the rank on all of them
        for index in matching_indexes:
            item = self.itemFromIndex(index)
            item.setData(rank, self.GROUP_RANK_ROLE)

        self.groups_modified.emit()

    def is_group_expanded(self, group_key):
        """ Returns true of the given group is expanded """
        if group_key not in self.__groups:
            raise KeyError("no group '%s'" % group_key)

        # expanded/collapsed is stored in the header
        header = self.__groups[group_key]["header"]
        if header.data(self.GROUP_VISIBLE_ROLE):
            return True
        return False

    def set_group_expanded(self, group_key, expanded):
        """ Set the given group to be expanded or not """
        if group_key not in self.__groups:
            raise KeyError("no group '%s'" % group_key)

        # store the expanded state in the header of the group
        header = self.__groups[group_key]["header"]
        header.setData(expanded, self.GROUP_VISIBLE_ROLE)

        # let listeners know that the state has changed
        self.group_toggled.emit(group_key, expanded)

    def get_expanded_state(self):
        state = {}
        for group_key in self.__groups.keys():
            header = self.__groups[group_key]["header"]
            state[group_key] = header.data(self.GROUP_VISIBLE_ROLE)
        return state

    def set_expanded_state(self, state):
        for (group_key, expanded) in state.iteritems():
            if group_key in self.__groups:
                self.set_group_expanded(group_key, expanded)

    # manage items
    def set_item_group(self, item, group_key):
        """ Put the given item in the given group """
        if group_key not in self.__groups:
            raise KeyError("no group '%s'" % group_key)

        # update the group and rank of the item
        rank = self.__groups[group_key]["header"].data(self.GROUP_RANK_ROLE)
        item.setData(group_key, self.GROUP_ROLE)
        item.setData(rank, self.GROUP_RANK_ROLE)

        self.groups_modified.emit()

    def get_item_group_key(self, item):
        """
        Get the group for the given item.
        If none has been set, return the default.
        """
        group_key = item.data(self.GROUP_ROLE)
        if group_key is None:
            return self.__default_group
        return group_key

    def is_content(self, item_or_index):
        """ Returns true if the given item or index is a content item """
        return self.__check_type(item_or_index, None)

    def is_header(self, item_or_index):
        """ Returns true if the given item or index is a header item """
        return self.__check_type(item_or_index, self.ITEM_TYPE_HEADER)

    def is_footer(self, item_or_index):
        """ Returns true if the given item or index is a footer item """
        return self.__check_type(item_or_index, self.ITEM_TYPE_FOOTER)

    def __get_items_of_type(self, row_type):
        # returns all items of a given type
        start = self.index(0, 0)
        match_flags = QtCore.Qt.MatchExactly
        return self.match(start, self.ITEM_TYPE_ROLE, row_type, -1,
                          match_flags)

    def __check_type(self, item_or_index, check_type):
        # check the type of the given item or index
        parent = item_or_index.parent()

        if isinstance(item_or_index,
                      QtGui.QStandardItem) and parent is not None:
            # item that is not top level
            item_type = None
        elif isinstance(item_or_index,
                        QtCore.QModelIndex) and parent.isValid():
            # index that is not top level
            item_type = None
        else:
            # top level item or index, type is stored directly
            item_type = item_or_index.data(self.ITEM_TYPE_ROLE)

        # return true if check_type matches
        if check_type is None:
            return item_type is None
        return item_type == check_type

    # react to data changes
    def __handle_rows_removed(self, parent, start, end):
        # if this is not a top level item there is nothing to do
        if not parent.isValid():
            return

        for row in xrange(start, end + 1):
            item = self.item(row, 0)
            group_key = item.data(self.GROUP_ROLE)
            item_type = item.data(self.ITEM_TYPE_ROLE)

            # if a header item is removed then remove the group
            if item_type == self.ITEM_TYPE_HEADER:
                del self.__groups[group_key]

            # if item is a footer then just remove the footer
            if item_type == self.ITEM_TYPE_FOOTER:
                del self.__groups[group_key]["footer"]

        self.groups_modified.emit()
Ejemplo n.º 20
0
    class WrappedClass(cls):
        """
        Wrapping class
        """

        # Emitted when something is dropped on the widget
        something_dropped = QtCore.Signal(list)

        def __init__(self, *args, **kwargs):
            """
            Instantiate the class and enable drops

            :param args: Arbitrary list of parameters used in base class init
            :param args: Arbitrary dictionary of parameters used in base class init
            """
            super(WrappedClass, self).__init__(*args, **kwargs)
            self.setAcceptDrops(True)
            self._set_property("dragging", False)
            self._restrict_to_ext = []

        # Override dragEnterEvent
        def dragEnterEvent(self, event):
            """
            Check if we can handle the drop, basically if we will receive local file path

            :param event: A QEvent
            """
            # Set a property which can used to change the look of the widget
            # in a style sheet using a pseudo state, e.g. :
            # DropAreaLabel[dragging="true"] {
            #     border: 2px solid white;
            # }
            if event.mimeData().hasFormat("text/plain"):
                event.accept()
            elif event.mimeData().hasFormat("text/uri-list"):
                for url in event.mimeData().urls():
                    # We don't activate the dragging state unless ext is valid.
                    self._set_property("dragging", True)
                    # Accept if there is at least one local file
                    if _is_local_file(url):
                        event.accept()
                        break
                else:
                    event.ignore()
            else:
                event.ignore()

        # Override dragLeaveEvent
        def dragLeaveEvent(self, event):
            """
            Just unset our dragging property

            :param event: A QEvent
            """
            self._set_property("dragging", False)

        # Override dropEvent
        def dropEvent(self, event):
            """
            Process a drop event, build a list of local files and emit somethingDropped if not empty

            :param event: A QEvent
            """
            self._set_property("dragging", False)
            urls = event.mimeData().urls()
            contents = None
            if urls:
                if sgtk.util.is_macos():
                    # Fix for Yosemite and later, file paths are not actual file paths
                    # but weird /.file/id=6571367.18855673 values
                    # https://bugreports.qt-project.org/browse/QTBUG-24379
                    # https://gist.github.com/wedesoft/3216298
                    try:
                        # Custom Python (e.g. brewed ones) might not be able to
                        # import Foundation. In that case we do nothing and keep
                        # urls as they are, assuming the problem should be fixed
                        # in custom Python / PyQt / PySide
                        import Foundation

                        fixed_urls = []
                        for url in urls:
                            # It is fine to pass a regular file url to this method
                            # e.g. file:///foo/bar/blah.ext
                            fu = Foundation.NSURL.URLWithString_(
                                url.toString()).filePathURL()
                            fixed_urls.append(QtCore.QUrl(str(fu)))
                        urls = fixed_urls
                    except:
                        pass
                contents = [x.toLocalFile() for x in urls if _is_local_file(x)]
            elif event.mimeData().hasFormat("text/plain"):
                contents = [event.mimeData().text()]
            if contents:
                event.accept()
                self.something_dropped.emit(contents)
            else:
                event.ignore()

        def _set_property(self, name, value):
            """
            Helper to set custom properties which can be used in style sheets
            Set the value and do a unpolish / polish to force an update

            :param name: A property name
            :param value: A value for this property
            """
            self.setProperty(name, value)
            # We are using a custom property in style sheets
            # we need to force a style sheet re-computation with
            # unpolish / polish
            self.style().unpolish(self)
            self.style().polish(self)
Ejemplo n.º 21
0
class SnapshotHistoryForm(QtGui.QWidget):

    restore = QtCore.Signal(QtGui.QWidget, basestring, basestring)
    snapshot = QtCore.Signal(QtGui.QWidget)
    closed = QtCore.Signal(QtGui.QWidget)

    def __init__(self, app, handler, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)

        self._app = app
        self._handler = handler

        self._path = ""

        # set up the UI
        from .ui.snapshot_history_form import Ui_SnapshotHistoryForm
        self._ui = Ui_SnapshotHistoryForm()
        self._ui.setupUi(self)

        self._ui.snapshot_list.set_app(self._app)
        self._ui.snapshot_list.selection_changed.connect(
            self._on_list_selection_changed)

        self._ui.snapshot_list.action_requested.connect(self._on_restore)
        self._ui.restore_btn.clicked.connect(self._on_restore)

        self._ui.snapshot_btn.clicked.connect(self._on_snapshot_btn_clicked)

    @property
    def path(self):
        return self._path

    def refresh(self):
        # clear the snapshot list:
        self._ui.snapshot_list.clear()

        # get the current path from the handler:
        self._path = None
        try:
            self._path = self._handler.get_current_file_path()
        except Exception, e:
            # this only ever happens when the scene operation hook
            # throws an exception!
            msg = ("Failed to find the current work file path:\n\n"
                   "%s\n\n"
                   "Unable to continue!" % e)
            self._ui.snapshot_list.set_message(msg)
            return

        # load file list:
        self._ui.snapshot_list.load({
            "handler": self._handler,
            "file_path": self._path
        })

        # update the browser title:
        self._ui.snapshot_list.set_label(
            self._handler.get_history_display_name(self._path))
Ejemplo n.º 22
0
class WorkerNotifier(QtCore.QObject):
    work_completed = QtCore.Signal(str, object)
    work_failure = QtCore.Signal(str, str)
Ejemplo n.º 23
0
class PublishForm(QtGui.QWidget):
    """
    Implementation of the main publish UI
    """

    # signals
    publish = QtCore.Signal()
    
    def __init__(self, app, handler, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)
        self._app = app
        
        # TODO: shouldn't need the handler
        self._handler = handler
    
        self._primary_task = None
        self._tasks = []
        
        # set up the UI
        from .ui.publish_form import Ui_PublishForm
        self._ui = Ui_PublishForm() 
        self._ui.setupUi(self)
        
        self._ui.publish_details.publish.connect(self._on_publish)
        self._ui.publish_details.cancel.connect(self._on_close)
        self._ui.publish_result.close.connect(self._on_close)
        
        expand_single_items = self._app.get_setting("expand_single_items")
        self._ui.publish_details.expand_single_items = expand_single_items
        
        allow_taskless_publishes = self._app.get_setting("allow_taskless_publishes")
        self._ui.publish_details.allow_no_task = allow_taskless_publishes
        
        self._ui.primary_error_label.setVisible(False)
        
        # always start with the details page:
        self.show_publish_details()
        
        # initialize:
        self._initialize()
        
    @property
    def selected_tasks(self):
        """
        The currently selected tasks
        """
        return self._get_selected_tasks()
    
    @property
    def shotgun_task(self):
        """
        The shotgun task that the publish should be linked to
        """
        return self._ui.publish_details.shotgun_task
        
    @property
    def thumbnail(self):
        """
        The thumbnail to use for the publish
        """
        return self._ui.publish_details.thumbnail
         
    @property
    def comment(self):
        """
        The comment to use for the publish
        """
        return self._ui.publish_details.comment
    
    def show_publish_details(self):
        self._ui.pages.setCurrentWidget(self._ui.publish_details)
        
    def show_publish_progress(self, title):
        self._ui.pages.setCurrentWidget(self._ui.publish_progress)
        self._ui.publish_progress.title = title
    
    def set_progress_reporter(self, reporter):
        self._ui.publish_progress.set_reporter(reporter)
        
    def show_publish_result(self, success, errors):
        """
        Show the result of the publish in the UI
        """
        # show page:
        self._ui.pages.setCurrentWidget(self._ui.publish_result)
        self._ui.publish_result.status = success
        self._ui.publish_result.errors = errors
        
    def _initialize(self):
        """
        Initialize UI with information provided
        """
        
        # pull initial data from handler:
        tasks = self._handler.get_publish_tasks()
        sg_tasks = self._handler.get_shotgun_tasks()
        thumbnail = self._handler.get_initial_thumbnail()
        sg_task = self._app.context.task
        
        # split tasks into primary and secondary:
        primary_task = None
        secondary_tasks = []
        for task in tasks:
            if task.output.is_primary:
                if primary_task:
                    # should never get this far but just in case!
                    raise Exception("Found multiple primary tasks - don't know how to handle this!")
                primary_task = task
            else:
                secondary_tasks.append(task)
                
        # initialize primary task UI:
        self._set_primary_task(primary_task)

        # initialize publish details form:
        self._ui.publish_details.initialize(secondary_tasks, sg_tasks)
        
        # set the initial thumbnail, comment and shotgun task
        self._ui.publish_details.comment = ""
        self._ui.publish_details.thumbnail = thumbnail
        self._ui.publish_details.shotgun_task = sg_task
        if sg_task:
            self._ui.publish_details.can_change_shotgun_task = False
         
    def _get_selected_tasks(self):
        """
        Get a list of the selected tasks that 
        should be published
        """
        
        # always publish primary task:
        selected_tasks = [self._primary_task]
        
        # get secondary tasks from details form:
        selected_tasks.extend(self._ui.publish_details.selected_tasks)
        
        return selected_tasks
        
    def _set_primary_task(self, task):
        """
        Set the primary task and update the UI accordingly
        """
        self._primary_task = task

        # connect to the primary tasks modified signal so that we can
        # update the UI if something changes.
        self._primary_task.modified.connect(self._on_primary_task_modified)
        
        # update UI for primary task:
        icon_path = self._primary_task.output.icon_path
        if os.path.isfile(icon_path) and os.path.exists(icon_path):
            icon = QtGui.QPixmap(icon_path)
            if not icon.isNull():
                self._ui.primary_icon_label.setPixmap(icon)
        
        # build details text and set:            
        lines = []
        name_str = self._primary_task.output.display_name
        if self._primary_task.item.name:
            name_str = "%s - %s" % (name_str, self._primary_task.item.name)
        lines.append("<span style='font-size: 16px'}><b>%s</b></span><span style='font-size: 12px'}>" % (name_str))
        if self._primary_task.output.description:
            lines.append("%s" % self._primary_task.output.description)
        if self._primary_task.item.description:
            lines.append("%s" % self._primary_task.item.description)
        details_txt = "%s</span>" % "<br>".join(lines) 
        self._ui.primary_details_label.setText(details_txt)
        
        # update errors text:
        self.__update_primary_errors()
        
    def _on_primary_task_modified(self):
        """
        Called when the primary task has been modified, e.g. there are new errors to report
        """
        # update the errors display for the primary publish
        self.__update_primary_errors()
        
    def __update_primary_errors(self):
        """
        Update the primary publish UI with any errors that were found during the pre-publish stage
        """
        if self._primary_task and self._primary_task.pre_publish_errors:
            error_txt = ("<font color='orange'>Validation checks returned some messages for your attention:"
                          "</font><br>%s" % "<br>".join(self._primary_task.pre_publish_errors))
            self._ui.primary_error_label.setText(error_txt)
            self._ui.primary_error_label.setVisible(True)
        else:
            self._ui.primary_error_label.setVisible(False)
        
        
    def _on_publish(self):
        """
        Slot called when the publish button in the dialog is clicked
        """
        self.publish.emit()
        
    def _on_close(self):
        """
        Slot called when the cancel or close signals in the dialog 
        are recieved
        """
        self.close()
        
        
        
        
        
        
        
        
        
Ejemplo n.º 24
0
class SnapshotHistoryForm(QtGui.QWidget):

    restore = QtCore.Signal(QtGui.QWidget, str, str)
    snapshot = QtCore.Signal(QtGui.QWidget)
    closed = QtCore.Signal(QtGui.QWidget)

    def __init__(self, app, handler, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)

        self._app = app
        self._handler = handler

        self._path = ""

        # set up the UI
        from .ui.snapshot_history_form import Ui_SnapshotHistoryForm

        self._ui = Ui_SnapshotHistoryForm()
        self._ui.setupUi(self)

        self._ui.snapshot_list.set_app(self._app)
        self._ui.snapshot_list.selection_changed.connect(
            self._on_list_selection_changed)

        self._ui.snapshot_list.action_requested.connect(self._on_restore)
        self._ui.restore_btn.clicked.connect(self._on_restore)

        self._ui.snapshot_btn.clicked.connect(self._on_snapshot_btn_clicked)

    @property
    def path(self):
        return self._path

    def refresh(self):
        # clear the snapshot list:
        self._ui.snapshot_list.clear()

        # get the current path from the handler:
        self._path = None
        try:
            self._path = self._handler.get_current_file_path()
        except Exception as e:
            # this only ever happens when the scene operation hook
            # throws an exception!
            msg = ("Failed to find the current work file path:\n\n"
                   "%s\n\n"
                   "Unable to continue!" % e)
            self._ui.snapshot_list.set_message(msg)
            return

        # load file list:
        self._ui.snapshot_list.load({
            "handler": self._handler,
            "file_path": self._path
        })

        # update the browser title:
        self._ui.snapshot_list.set_label(
            self._handler.get_history_display_name(self._path))

    def closeEvent(self, event):
        """
        Called when the widget is closed.
        """
        # make sure the snapshot list BrowserWidget is
        # cleaned up properly
        self._ui.snapshot_list.destroy()

        # emit closed event:
        self.closed.emit(self)

        return QtGui.QWidget.closeEvent(self, event)

    def event(self, event):
        """
        override event to cause UI to reload the first time it is shown:
        """
        if event.type() == QtCore.QEvent.Polish:
            self.refresh()
        return QtGui.QWidget.event(self, event)

    def _on_list_selection_changed(self):
        self._update_ui()

    def _on_restore(self):
        path = self._ui.snapshot_list.get_selected_path()
        self.restore.emit(self, self._path, path)

    def _on_snapshot_btn_clicked(self):
        self.snapshot.emit(self)

    def _update_ui(self):
        can_restore = self._ui.snapshot_list.get_selected_item() != None
        self._ui.restore_btn.setEnabled(can_restore)
Ejemplo n.º 25
0
class BrowserWidget(QtGui.QWidget):
    
    ######################################################################################
    # SIGNALS
    
    # when the selection changes 
    selection_changed = QtCore.Signal()
    
    # when someone double clicks on an item
    action_requested = QtCore.Signal()
    
    # called when the list contents have been modified:
    list_modified = QtCore.Signal()
    
    
    ######################################################################################
    # Init & Destruct
    
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        
        # set up the UI
        self.ui = Ui_Browser() 
        self.ui.setupUi(self)

        # hide the overlays
        self.ui.main_pages.setCurrentWidget(self.ui.items_page)

        self._app = None
        self._worker = None
        self._current_work_id = None
        self._dynamic_widgets = []
        self._multi_select = False
        self._search = True
        self._last_item_to_show = None
        
        # spinner
        self._spin_icons = []
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_1.png"))
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_2.png"))
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_3.png"))
        self._spin_icons.append(QtGui.QPixmap(":/res/progress_bar_4.png")) 
        self._timer = QtCore.QTimer(self)
        self._timer.timeout.connect( self._update_spinner )
        self._current_spinner_index = 0
        
        # search
        self.ui.search.textEdited.connect(self._on_search_text_changed)
        
        # style:
        self._title_base_style = {
            "border":"none",
            "border-color":"rgb(32,32,32)",
            "border-top-left-radius":"3px",
            "border-top-right-radius":"3px",
            "border-bottom-left-radius":"0px",
            "border-bottom-right-radius":"0px"
        }
        self._title_styles = {
            "gradient":{"background":"qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(97, 97, 97, 255), stop:1 rgba(49, 49, 49, 255));"},
            "none":{}
        }
        self._title_margins = {
            "gradient":[12,3,12,3],
            "none":[3,3,3,3]
        }
        
        self._current_title_style = "none"
        self.title_style = "gradient"
        
    @property
    def title_style(self):
        return self._current_title_style
    @title_style.setter
    def title_style(self, value):
        if value != self._current_title_style and value in self._title_styles.keys():
            # change style sheet:
            self._current_title_style = value
            style = self._title_base_style.copy()
            style.update(self._title_styles[self._current_title_style])
            ss = self._style_as_string("#browser_header", style)
            self.ui.browser_header.setStyleSheet(ss)
            
            # change margins:
            margins = self._title_margins.get(self._current_title_style)
            if margins:
                self.ui.browser_header.layout().setContentsMargins(margins[0], margins[1], margins[2], margins[3])
        
    def enable_multi_select(self, enable):
        """
        Should we enable multi select
        """
        self._multi_select = enable
        
    def enable_search(self, status):
        """
        Toggle the search bar (on by default)
        """
        self.ui.search.setVisible(status)
        
    def destroy(self):
        if self._worker:
            self._worker.stop()
        
    def set_app(self, app):
        """
        associate with an app object
        """
        self._app = app
        # set up worker queue
        self._worker = Worker(app)
        self._worker.work_completed.connect( self._on_worker_signal)
        self._worker.work_failure.connect( self._on_worker_failure)
        
        self._worker.start()
        
    def set_label(self, label):
        """
        Sets the text next to the search button 
        """
        self.ui.label.setText("<big>%s</big>" % label)
    ######################################################################################
    # Public Methods
    
    def load(self, data):
        """
        Loads data into the browser widget. 
        Called by outside code 
        """
        # start spinning
        self.ui.main_pages.setCurrentWidget(self.ui.loading_page)
        self._timer.start(100)
        # queue up work
        self._current_work_id = self._worker.queue_work(self.get_data, data, asap=True)
        
        self.list_modified.emit()
    
    def clear(self):
        """
        Clear widget of its contents.
        """
        # hide overlays
        self.ui.main_pages.setCurrentWidget(self.ui.items_page)
        
        # clear search box
        self.ui.search.setText("")
        
        # also reset any jobs that are processing. No point processing them
        # if their requestors are gone.
        if self._worker:
            self._worker.clear()
            
        for x in self._dynamic_widgets:
            # remove widget from layout:
            self.ui.scroll_area_layout.removeWidget(x)
            # set it's parent to None so that it is removed from the widget hierarchy
            x.setParent(None)
            # mark it to be deleted when event processing returns to the main loop
            x.deleteLater()
        self._dynamic_widgets = []
        
        self.list_modified.emit()
        
            
    def set_message(self, message):
        """
        Replace the list of items with a single message
        """
        self.ui.main_pages.setCurrentWidget(self.ui.status_page)
        self.ui.status_message.setText(message)
        
    def clear_selection(self):
        """
        Clears the selection
        """
        for x in self._dynamic_widgets:
            x.set_selected(False)        
                
    def get_selected_item(self):
        """
        Gets the last selected item, None if no selection
        """
        for widget in self._dynamic_widgets:
            if widget.is_selected():
                return widget
        return None
    
    def get_selected_items(self):
        """
        Returns entire selection
        """
        selected_items = []
        for widget in self._dynamic_widgets:
            if widget.is_selected():
                selected_items.append(widget)
        return selected_items
        
    def get_items(self):
        return self._dynamic_widgets
        
    def select(self, item):
        """
        Select an item and ensure it is visible
        """
        self._on_item_clicked(item)
        self._ensure_item_is_visible(item)
    
    ##########################################################################################
    # Protected stuff - implemented by deriving classes
    
    def get_data(self, data):
        """
        Needs to be implemented by subclasses
        """
        raise Exception("not implemented!")
    
    def process_result(self, result):
        """
        Needs to be implemented by subclasses
        """
        raise Exception("not implemented!")
    
    ##########################################################################################
    # Internals
    
    def _style_as_string(self, name, style_dict):
        style_elements = ["%s: %s;" % (key, value) for key, value in style_dict.iteritems()] 
        return "%s { %s }" % (name, "".join(style_elements)) 
    
    def _on_search_text_changed(self, text):
        """
        Cull based on search box
        """

        if text == "":
            # show all items
            for i in self._dynamic_widgets:
                i.setVisible(True)

        elif len(text) > 2: # cull by string for strings > 2 chars
            
            # if running PyQt, convert QString to str
            if not isinstance(text, basestring):
                # convert QString to str
                text = str(text)
            
            # now we have a str or unicode object which has the lower() method
            lower_text = text.lower()  
            
            for i in self._dynamic_widgets:
                
                details = i.get_details()

                # if running PyQt, convert QString to str
                details_lower = details
                if not isinstance(details_lower, basestring):
                    details_lower = str(details_lower)
                # now we have a str or unicode object which has the lower() method
                details_lower = details_lower.lower()
                
                if details is None: # header
                    i.setVisible(True)
                
                elif lower_text in details_lower: 
                    i.setVisible(True)
                    
                else:
                    i.setVisible(False)
    
    def _on_worker_failure(self, uid, msg):
        """
        The worker couldn't execute stuff
        """
        if self._current_work_id != uid:
            # not our job. ignore
            return

        # finally, turn off progress indication and turn on display
        self.ui.main_pages.setCurrentWidget(self.ui.items_page)
        self._timer.stop()
    
        # show error message
        self.set_message(msg)
        

    def _on_worker_signal(self, uid, data):
        """
        Signalled whenever the worker completes something
        """
        if self._current_work_id != uid:
            # not our job. ignore
            return
    
        # process!
        self.process_result(data)

        # if currently showing progress, switch to items page:
        if self.ui.main_pages.currentWidget() == self.ui.loading_page:
            self.ui.main_pages.setCurrentWidget(self.ui.items_page)
        
        # stop timer:
        self._timer.stop()
        
        # if something was 'selected' whilst doing work then
        # ensure it is visible:
        if self._last_item_to_show:
            self._ensure_item_is_visible(self._last_item_to_show)
            self._last_item_to_show = None
        
        # and just in case the list has been modified
        self.list_modified.emit()
            
    
    def _update_spinner(self):
        """
        Animate spinner icon
        """
        self.ui.progress_bar.setPixmap(self._spin_icons[self._current_spinner_index])
        self._current_spinner_index += 1
        if self._current_spinner_index == 4:
            self._current_spinner_index = 0            
        
    def _ensure_item_is_visible(self, item):
        """
        Ensure the item is visible by scrolling to it.
        """
        # check that the items page is the current page.  If
        # it's not then we just keep track of the item to be 
        # scrolled to when all current work is completed
        if self.ui.main_pages.currentWidget() != self.ui.items_page:
            self._last_item_to_show = item
        else:
            # in order for the scroll to happen during load, first give
            # the scroll area  chance to resize it self by processing its event queue.
            QtCore.QCoreApplication.processEvents()
            # and focus on the selection
            self.ui.scroll_area.ensureWidgetVisible(item)
            
        
    def _on_item_clicked(self, item):
        
        if item.supports_selection() == False:
            # not all items are selectable
            return
        
        if self._multi_select:
            # invert selection:
            item.set_selected(not item.is_selected())
        else:
            # single select
            self.clear_selection()
            item.set_selected(True)
            
        self.selection_changed.emit()

    def _on_item_double_clicked(self, item):
        self.action_requested.emit()

    def add_item(self, item_class):
        """
        Adds a list item. Returns the created object.
        """
        widget = item_class(self._app, self._worker, self)
        self.ui.scroll_area_layout.addWidget(widget)
        self._dynamic_widgets.append(widget)   
        widget.clicked.connect( self._on_item_clicked )
        widget.double_clicked.connect( self._on_item_double_clicked )  
        
        self.list_modified.emit()
         
        return widget  
Ejemplo n.º 26
0
class HotKeyEditor(QtGui.QLineEdit):
    # Emitted when the key sequence changes.
    # The first argument is the key sequence.
    # The second argument is the native modifiers associated with the sequence
    # The third argument is the native key code associated with the sequence
    key_sequence_changed = QtCore.Signal(QtGui.QKeySequence, int, int)

    def __init__(self, parent=None):
        QtGui.QLineEdit.__init__(self, parent)

        self.__key_sequence = QtGui.QKeySequence()

        # self.__line_edit.installEventFilter(self)
        self.setReadOnly(True)
        self.setAttribute(QtCore.Qt.WA_InputMethodEnabled)

    def eventFilter(self, widget, event):
        if widget == self and event.type() == QtCore.QEvent.ContextMenu:
            menu = self.createStandardContextMenu()
            actions = menu.actions()
            for action in actions:
                action.setShortcut(QtGui.QKeySequence())
                action_string = action.text()
                pos = action_string.rfind("\t")
                if pos > 0:
                    action_string = action_string[:pos]
                    action.setText(action_string)

            action_before = None
            if len(actions) > 0:
                action_before = actions[0]
            clear = QtGui.QAction("Clear Shortcut", menu)
            menu.insertAction(action_before, clear)
            menu.insertSeparator(action_before)
            clear.setEnabled(not self.__key_sequence.isEmpty())
            clear.triggered.connect(self.clear_shortcut)
            menu.exec_(event.globalPos())
            del menu
            event.accept()
            return True

        return QtGui.QLineEdit.eventFilter(self, widget, event)

    def clear_shortcut(self):
        if self.__key_sequence.isEmpty():
            return

        self.key_sequence = QtGui.QKeySequence()
        self.key_sequence_changed.emit(self.key_sequence, 0, 0)

    def handle_key_event(self, event):
        if event.isAutoRepeat():
            return

        key = event.key()
        if (key == QtCore.Qt.Key_Control or key == QtCore.Qt.Key_Shift
                or key == QtCore.Qt.Key_Meta or key == QtCore.Qt.Key_Alt
                or key == QtCore.Qt.Key_Super_L or key == QtCore.Qt.Key_AltGr):
            return

        if not event.modifiers():
            return

        key |= self.translate_modifiers(event.modifiers(), event.text())
        self.key_sequence = QtGui.QKeySequence(key)
        self.key_sequence_changed.emit(
            self.key_sequence,
            event.nativeModifiers(),
            event.nativeVirtualKey(),
        )

        event.accept()

    @property
    def key_sequence(self):
        return self.__key_sequence

    @key_sequence.setter
    def key_sequence(self, sequence):
        if sequence == self.__key_sequence:
            return

        self.__key_sequence = sequence
        self.setText(
            self.__key_sequence.toString(QtGui.QKeySequence.NativeText))

    def translate_modifiers(self, state, text):
        result = 0
        if state & QtCore.Qt.ShiftModifier:
            if len(text) == 0:
                result |= QtCore.Qt.SHIFT
            else:
                category = unicodedata.category(text[0])
                if category[0] in ["L", "N", "P", "Z"]:
                    result |= QtCore.Qt.SHIFT
        if state & QtCore.Qt.ControlModifier:
            result |= QtCore.Qt.CTRL
        if state & QtCore.Qt.MetaModifier:
            result |= QtCore.Qt.META
        if state & QtCore.Qt.AltModifier:
            result |= QtCore.Qt.ALT
        return result

    def focusInEvent(self, event):
        QtGui.QLineEdit.focusInEvent(self, event)
        self.selectAll()

    def keyPressEvent(self, event):
        self.handle_key_event(event)
        event.accept()

    def event(self, event):
        if (event.type() == QtCore.QEvent.Shortcut
                or event.type() == QtCore.QEvent.ShortcutOverride
                or event.type() == QtCore.QEvent.KeyRelease):
            event.accept()
            return True

        return QtGui.QLineEdit.event(self, event)
Ejemplo n.º 27
0
class ShotgunOverlayModel(ShotgunModel):
    """
    Convenience wrapper around the ShotgunModel which adds
    spinner and error reporting overlay functionality. 
    """

    # signal that gets emitted whenever the model deems it appropriate to
    # indicate that data is being loaded. Note that this signal is not
    # emitted every time data is loaded from Shotgun, but only when there
    # is no cached data available to display. This signal can be useful if
    # an implementation wants to set up a custom overlay system instead
    # of or in addition to the built in one that is provided via
    # the set_overlay_parent() method.
    progress_spinner_start = QtCore.Signal()

    # conversely, an end signal is being emitted every time a progress spinner
    # should be deactivated.
    progress_spinner_end = QtCore.Signal()

    def __init__(self,
                 parent,
                 overlay_widget,
                 download_thumbs=True,
                 schema_generation=0,
                 bg_load_thumbs=False):
        """
        Constructor. This will create a model which can later be used to load
        and manage Shotgun data.
        
        :param parent: Parent object.
        :param overlay_widget: Widget on which the spinner/info overlay should be positioned.
        :param download_thumbs: Boolean to indicate if this model should attempt 
                                to download and process thumbnails for the downloaded data.
        :param schema_generation: Schema generation index. If you are changing the format 
                                  of the data you are retrieving from Shotgun, and therefore
                                  want to invalidate any cache files that may already exist
                                  in the system, you can increment this integer.
        :param bg_load_thumbs: If set to True, thumbnails will be loaded in the background.
        
        """
        ShotgunModel.__init__(self, parent, download_thumbs, schema_generation,
                              bg_load_thumbs)

        # set up our spinner UI handling
        self.__overlay = overlay_module.ShotgunOverlayWidget(overlay_widget)
        self._is_in_spin_state = False
        self._cache_loaded = False

        # set up some model signals etc.
        self.data_refreshed.connect(self.__on_data_refreshed)
        self.data_refresh_fail.connect(self.__on_data_refresh_fail)

    ########################################################################################
    # protected methods not meant to be subclassed but meant to be called by subclasses

    def _load_data(self,
                   entity_type,
                   filters,
                   hierarchy,
                   fields,
                   order=None,
                   seed=None,
                   limit=None):
        """
        Overridden from ShotgunModel. 
        """
        # reset overlay
        self.__overlay.hide(hide_errors=True)
        # call base class
        self._cache_loaded = ShotgunModel._load_data(self, entity_type,
                                                     filters, hierarchy,
                                                     fields, order, seed,
                                                     limit)
        return self._cache_loaded

    def _refresh_data(self):
        """
        Overridden from ShotgunModel.
        """
        if not self._cache_loaded:
            # we are doing asynchronous loading into an uncached model.
            # start spinning
            self._show_overlay_spinner()
        # call base class
        return ShotgunModel._refresh_data(self)

    def _show_overlay_spinner(self):
        """
        Shows the overlay spinner
        """
        self.__overlay.start_spin()
        # signal to any external listeners
        self.progress_spinner_start.emit()
        self._is_in_spin_state = True

    def _hide_overlay_info(self):
        """
        Hides any overlay that is currently shown, except for error messages.
        """
        self.__overlay.hide(hide_errors=False)

    def _show_overlay_pixmap(self, pixmap):
        """
        Show an overlay status message in the form of a pixmap.
        This is for example useful if a particular query doesn't return any results.
        If an error message is already being shown, the pixmap will not 
        replace the error message. 
        
        :param pixmap: QPixmap object containing graphic to show.
        """
        self.__overlay.show_message_pixmap(pixmap)

    def _show_overlay_info_message(self, msg):
        """
        Show an overlay status message.
        If an error is already displayed, 
        this info message will not be shown.
        
        :param msg: message to display
        :returns: True if the message was shown, False if not.
        """
        self.__overlay.show_message(msg)

    def _show_overlay_error_message(self, msg):
        """
        Show an overlay error message.
        
        :param msg: error message to display
        """
        self.__overlay.show_error_message(msg)

    ########################################################################################
    # private methods

    def __on_data_refreshed(self):
        """
        Callback when async data has arrived successfully
        """
        self._cache_loaded = True
        if self._is_in_spin_state:
            self.__overlay.hide(hide_errors=True)
            # we are spinning, so signal the spin to end
            self._is_in_spin_state = False
            self.progress_spinner_end.emit()

    def __on_data_refresh_fail(self, msg):
        """
        Callback when async data has failed to arrive 
        """
        self.__overlay.show_error_message(msg)
        if self._is_in_spin_state:
            # we are spinning, so signal the spin to end
            self._is_in_spin_state = False
            self.progress_spinner_end.emit()
Ejemplo n.º 28
0
class ShotgunAsyncDataRetriever(QtCore.QThread):
    """
    Note: This is part of the internals of the Shotgun Utils Framework and 
    should not be called directly.
    
    Async worker class which is used by the ShotgunModel to retrieve data 
    and thumbnails from Shotgun and from disk thumbnail cache.
    
    Tasks are queued up using the execute_find() and request_thumbnail() methods.
    
    Tasks are executed in the following priority order:
    
    - first any thumbnails that are already cached on disk are handled
    - next, shotgun find() queries are handled
    - lastly thumbnail downloads are handled  
    
    The thread will emit work_completed and work_failure signals
    when tasks are completed (or fail).
    
    The clear() method will clear the current queue. The currently 
    processing item will finish processing and may send out signals
    even after a clear.
    
    Make sure you call the stop() method prior to destruction in order 
    for the system to gracefully shut down.
    """

    # async task types
    THUMB_CHECK, SG_FIND_QUERY, THUMB_DOWNLOAD = range(3)

    work_completed = QtCore.Signal(str, dict)
    work_failure = QtCore.Signal(str, str)

    def __init__(self, parent=None):
        """
        Construction
        """
        QtCore.QThread.__init__(self, parent)
        self._app = tank.platform.current_bundle()
        self._wait_condition = QtCore.QWaitCondition()
        self._queue_mutex = QtCore.QMutex()
        self.__sg = None

        # queue data structures
        self._thumb_download_queue = []
        self._sg_find_queue = []
        self._thumb_check_queue = []

        # indicates that we should keep processing queue items
        self._process_queue = True

    ############################################################################################################
    # Public methods

    def set_shotgun_connection(self, sg):
        """
        Specify the shotgun api instance this model should use to communicate
        with Shotgun. If not specified, each model instance will instantiante its
        own connection, via toolkit. The behaviour where each model has its own
        connection is generally recommended for thread safety reasons since 
        the Shotgun API isn't natively threadsafe.
        
        :param sg: Shotgun API instance
        """
        self.__sg = sg

    def clear(self):
        """
        Clears the queue. Any currently processing item will complete without interruption.
        """
        self._queue_mutex.lock()
        try:
            self._app.log_debug(
                "%s: Clearing queue. Discarded items: SG api requests: [%s] Thumb checks: [%s] "
                "Thumb downloads: [%s]" %
                (self, len(self._sg_find_queue), len(
                    self._thumb_check_queue), len(self._thumb_download_queue)))
            self._thumb_download_queue = []
            self._sg_find_queue = []
            self._thumb_check_queue = []
        finally:
            self._queue_mutex.unlock()

    def stop(self):
        """
        Gracefully stop the thread. Will synchronounsly wait until any potential
        currently processing item is completing.
        """
        self._process_queue = False
        self._wait_condition.wakeAll()
        self.wait()

    def execute_find(self, entity_type, filters, fields, order=None):
        """
        Adds a find query to the queue. 
        
        :param entity_type: Shotgun entity type
        :param filters: List of find filters to pass to Shotgun find call
        :param fields: List of fields to pass to Shotgun find call
        :param order: List of order dicts to pass to Shotgun find call
        :returns: A unique identifier representing this request 
        """
        uid = uuid.uuid4().hex

        work = {
            "id": uid,
            "entity_type": entity_type,
            "filters": filters,
            "fields": fields,
            "order": order
        }
        self._queue_mutex.lock()
        try:
            self._sg_find_queue.append(work)
        finally:
            self._queue_mutex.unlock()

        # wake up execution loop!
        self._wait_condition.wakeAll()

        return uid

    def request_thumbnail(self, url, entity_type, entity_id, field):
        """
        Adds a Shotgun thumbnail request to the queue. 
        
        :param url: The thumbnail url that is associated with this thumbnail
        :param entity_type: Shotgun entity type with which the thumb is associated.
        :param entity_id: Shotgun entity id with which the thumb is associated.
        :param field: Thumbnail field. Normally 'image' but could also for example
                      be a deep link field such as 'sg_sequence.Sequence.image'
        
        :returns: A unique identifier representing this request 
        """
        uid = uuid.uuid4().hex

        work = {
            "id": uid,
            "url": url,
            "field": field,
            "entity_type": entity_type,
            "entity_id": entity_id
        }
        self._queue_mutex.lock()
        try:
            self._thumb_check_queue.append(work)
        finally:
            self._queue_mutex.unlock()

        # wake up execution loop!
        self._wait_condition.wakeAll()

        return uid

    ############################################################################################################
    # Internal methods

    def _get_thumbnail_path(self, url):
        """
        Returns the location on disk suitable for a thumbnail given its url.
        """

        url_obj = urlparse.urlparse(url)
        url_path = url_obj.path
        path_chunks = url_path.split("/")

        CHUNK_LEN = 16

        # post process the path
        # old (pre-S3) style result:
        # path_chunks: [ "", "thumbs", "1", "2", "2.jpg"]

        # s3 result, form 1:
        # path_chunks: [u'',
        #               u'9902b5f5f336fae2fb248e8a8748fcd9aedd822e',
        #               u'be4236b8f198ae84df2366920e7ee327cc0a567e',
        #               u'render_0400_t.jpg']

        # s3 result, form 2:
        # path_chunks: [u'', u'thumbnail', u'api_image', u'150']

        def _to_chunks(s):
            #split the string 'abcdefghxx' into ['abcdefgh', 'xx']
            chunks = []
            for start in range(0, len(s), CHUNK_LEN):
                chunks.append(s[start:start + CHUNK_LEN])
            return chunks

        new_chunks = []
        for folder in path_chunks[:-1]:  # skip the file name
            if folder == "":
                continue
            if len(folder) > CHUNK_LEN:
                # long url path segment like 9902b5f5f336fae2fb248e8a8748fcd9aedd822e
                # split it into chunks for 4
                new_chunks.extend(_to_chunks(folder))
            else:
                new_chunks.append(folder)

        # establish the root path
        cache_path_items = [self._app.cache_location, "thumbnails"]
        # append the folders
        cache_path_items.extend(new_chunks)
        # and append the file name
        # all sg thumbs are jpegs so append extension too - some url forms don't have this.
        cache_path_items.append("%s.jpeg" % path_chunks[-1])

        # join up the path
        path_to_cached_thumb = os.path.join(*cache_path_items)

        return path_to_cached_thumb

    ############################################################################################
    # main thread loop

    def run(self):
        """
        Main thread loop
        """

        if self.__sg is None:
            # create our own private shotgun connection. This is because
            # the shotgun API isn't threadsafe, so running multiple models in parallel
            # (common) may result in side effects if a single connection is shared
            self.__sg = tank.util.shotgun.create_sg_connection()

            # set the maximum timeout for this connection for fluency
            self.__sg.config.timeout_secs = CONNECTION_TIMEOUT_SECS

        # keep running until thread is terminated
        while self._process_queue:

            # Step 1. get the next item to process.
            # We check things in the following priority order:
            # - If there is anything in the thumb check queue, do that first
            # - Then check sg queue
            # - Lastly, check thumb downloads
            item_to_process = None
            item_type = None
            self._queue_mutex.lock()
            try:

                if len(self._thumb_check_queue) > 0:
                    item_to_process = self._thumb_check_queue.pop(0)
                    item_type = ShotgunAsyncDataRetriever.THUMB_CHECK

                elif len(self._sg_find_queue) > 0:
                    item_to_process = self._sg_find_queue.pop(0)
                    item_type = ShotgunAsyncDataRetriever.SG_FIND_QUERY

                elif len(self._thumb_download_queue) > 0:
                    item_to_process = self._thumb_download_queue.pop(0)
                    item_type = ShotgunAsyncDataRetriever.THUMB_DOWNLOAD

                else:
                    # no work to be done!
                    # wait for some more work - this unlocks the mutex
                    # until the wait condition is signalled where it
                    # will then attempt to obtain a lock before returning
                    self._wait_condition.wait(self._queue_mutex)
                    # once the wait condition is triggered (usually by something
                    # inserted into one of the queues), trigger the check to happen again
                    continue

            finally:
                self._queue_mutex.unlock()

            # Step 2. Process next item and send signals.
            try:

                # process the item:

                if item_type == ShotgunAsyncDataRetriever.SG_FIND_QUERY:
                    # get stuff from shotgun
                    sg = self.__sg.find(item_to_process["entity_type"],
                                        item_to_process["filters"],
                                        item_to_process["fields"],
                                        item_to_process["order"])
                    # need to wrap it in a dict not to confuse pyqt's signals and type system
                    self.work_completed.emit(item_to_process["id"], {"sg": sg})

                elif item_type == ShotgunAsyncDataRetriever.THUMB_CHECK:
                    # check if a thumbnail exists on disk. If not, fall back onto
                    # a thumbnail download from shotgun/s3
                    url = item_to_process["url"]
                    path_to_cached_thumb = self._get_thumbnail_path(url)
                    if os.path.exists(path_to_cached_thumb):
                        # thumbnail already here! yay!
                        self.work_completed.emit(
                            item_to_process["id"],
                            {"thumb_path": path_to_cached_thumb})
                    else:
                        # no thumb here. Stick the data into the thumb download queue to request download
                        self._queue_mutex.lock()
                        try:
                            self._thumb_download_queue.append(item_to_process)
                        finally:
                            self._queue_mutex.unlock()

                elif item_type == ShotgunAsyncDataRetriever.THUMB_DOWNLOAD:
                    # download the actual thumbnail. Because of S3, the url
                    # has most likely expired, so need to re-fetch it via a sg find
                    entity_id = item_to_process["entity_id"]
                    entity_type = item_to_process["entity_type"]
                    field = item_to_process["field"]

                    sg_data = self.__sg.find_one(entity_type,
                                                 [["id", "is", entity_id]],
                                                 [field])

                    if sg_data is None or sg_data.get(field) is None:
                        # no thumbnail! This is possible if the thumb has changed
                        # while we were queueing it for download. In this case
                        # simply don't do anything
                        pass

                    else:
                        # download from sg
                        url = sg_data[field]
                        path_to_cached_thumb = self._get_thumbnail_path(url)
                        self._app.ensure_folder_exists(
                            os.path.dirname(path_to_cached_thumb))
                        tank.util.download_url(self._app.shotgun, url,
                                               path_to_cached_thumb)
                        # modify the permissions of the file so it's writeable by others
                        old_umask = os.umask(0)
                        try:
                            os.chmod(path_to_cached_thumb, 0666)
                        finally:
                            os.umask(old_umask)

                        self.work_completed.emit(
                            item_to_process["id"],
                            {"thumb_path": path_to_cached_thumb})

                else:
                    raise Exception("Unknown task type!")

            except Exception, e:
                self.work_failure.emit(item_to_process["id"],
                                       "An error occurred: %s" % e)
Ejemplo n.º 29
0
class PublishDetailsForm(QtGui.QWidget):
    """
    Implementation of the main publish UI
    """

    # signals
    publish = QtCore.Signal()
    cancel = QtCore.Signal()

    def __init__(self, parent=None):
        """
        Construction
        """
        QtGui.QWidget.__init__(self, parent)

        self.expand_single_items = False
        self.allow_no_task = False

        self._group_widget_info = {}
        self._tasks = []

        # set up the UI
        from .ui.publish_details_form import Ui_PublishDetailsForm
        self._ui = Ui_PublishDetailsForm()
        self._ui.setupUi(self)

        # create vbox layout for scroll widget:
        layout = QtGui.QVBoxLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(2, 2, 2, 2)
        self._ui.task_scroll.widget().setLayout(layout)

        # hook up buttons
        self._ui.publish_btn.clicked.connect(self._on_publish)
        self._ui.cancel_btn.clicked.connect(self._on_cancel)

        self.can_change_shotgun_task = True

    @property
    def selected_tasks(self):
        return self._get_selected_tasks()

    # @property
    def __get_shotgun_task(self):
        return self._get_sg_task_combo_task(
            self._ui.sg_task_combo.currentIndex())

    # @shotgun_task.setter
    def __set_shotgun_task(self, value):
        self._set_current_shotgun_task(value)

    shotgun_task = property(__get_shotgun_task, __set_shotgun_task)

    # @property
    def __get_comment(self):
        return self._safe_to_string(
            self._ui.comments_edit.toPlainText()).strip()

    # @comment.setter
    def __set_comment(self, value):
        self._ui.comments_edit.setPlainText(value)

    comment = property(__get_comment, __set_comment)

    # @property
    def __get_thumbnail(self):
        return self._ui.thumbnail_widget.thumbnail

    # @thumbnail.setter
    def __set_thumbnail(self, value):
        self._ui.thumbnail_widget.thumbnail = value

    thumbnail = property(__get_thumbnail, __set_thumbnail)

    # @property
    def __get_can_change_shotgun_task(self):
        """
        Control if the shotgun task can be changed or not
        """
        return self._ui.sg_task_stacked_widget.currenWidget(
        ) == self._ui.sg_task_menu_page

    # @can_change_shotgun_task.setter
    def __set_can_change_shotgun_task(self, value):
        page = None
        header_txt = ""
        if value:
            page = self._ui.sg_task_menu_page
            header_txt = "What Shotgun Task are you working on?"
        else:
            page = self._ui.sg_task_label_page
            header_txt = "The Publish will be associated with Shotgun Task:"

        self._ui.sg_task_stacked_widget.setCurrentWidget(page)
        self._ui.task_header_label.setText(header_txt)

    can_change_shotgun_task = property(__get_can_change_shotgun_task,
                                       __set_can_change_shotgun_task)

    def initialize(self, tasks, sg_tasks):
        """
        Initialize UI
        """
        # reset UI to default state:
        self._ui.sg_task_combo.setEnabled(True)

        # populate shotgun task list:
        self._populate_shotgun_tasks(sg_tasks)

        # connect up modified signal on tasks:
        self._tasks = tasks

        # populate outputs list:
        self._populate_task_list()

    def _get_sg_task_combo_task(self, index):
        """
        Get the shotgun task for the currently selected item in the task combo
        """
        task = self._ui.sg_task_combo.itemData(index) if index >= 0 else None
        if task:
            if hasattr(QtCore, "QVariant") and isinstance(
                    task, QtCore.QVariant):
                task = task.toPyObject()

            # task is a wrapped object to avoid PyQt QString conversion fun!
            if task:
                task = task.obj

        return task

    def _populate_shotgun_tasks(self, sg_tasks):
        """
        Populate the shotgun task combo box with the provided
        list of shotgun tasks
        """
        current_task = self._get_sg_task_combo_task(
            self._ui.sg_task_combo.currentIndex())
        self._ui.sg_task_combo.clear()

        # add 'no task' task:
        if self.allow_no_task:
            self._ui.sg_task_combo.addItem(
                "Do not associate this publish with a task")
            self._ui.sg_task_combo.insertSeparator(
                self._ui.sg_task_combo.count())
            self._ui.sg_task_combo.insertSeparator(
                self._ui.sg_task_combo.count())

        # add tasks:
        for task in sg_tasks:
            label = "%s, %s" % (task["step"]["name"], task["content"])
            self._ui.sg_task_combo.addItem(label, _ObjWrapper(task))

        # reselect selected task if it is still in list:
        self._set_current_shotgun_task(current_task)

    def _set_current_shotgun_task(self, task):
        """
        Select the specified task in the shotgun task
        combo box
        """

        # update the selection combo:
        found_index = None
        for ii in range(0, self._ui.sg_task_combo.count()):
            item_task = self._get_sg_task_combo_task(ii)

            found = False
            if not task:
                found = not item_task
            elif item_task:
                found = task["id"] == item_task["id"]

            if found:
                found_index = ii
                break
        self._ui.sg_task_combo.setCurrentIndex(found_index or 0)

        # also update the static label:
        label = "None!"
        if found_index != None:
            label = self._ui.sg_task_combo.itemText(found_index)
        self._ui.sg_task_label.setText(label)

    def _populate_task_list(self):
        """
        Build the main task list for selection of outputs, items, etc.
        """

        # clear existing widgets:
        task_scroll_widget = self._ui.task_scroll.widget()
        self._group_widget_info = {}
        #TODO

        if len(self._tasks) == 0:
            # no tasks so show no tasks text:
            self._ui.publishes_stacked_widget.setCurrentWidget(
                self._ui.no_publishes_page)
            return
        else:
            self._ui.publishes_stacked_widget.setCurrentWidget(
                self._ui.publishes_page)

        # group tasks by display group:
        group_order = []
        tasks_by_group = {}
        for task in self._tasks:
            group = tasks_by_group.setdefault(task.output.display_group,
                                              dict())

            # track unique outputs for this group maintaining order
            # respective to task
            group_outputs = group.setdefault("outputs", list())
            if task.output not in group_outputs:
                group_outputs.append(task.output)

            # track unique items for this group maintaining order
            # respective to task
            group_items = group.setdefault("items", list())
            if task.item not in group_items:
                group_items.append(task.item)

            # track tasks for this group:
            group.setdefault("tasks", list()).append(task)

            if not task.output.display_group in group_order:
                group_order.append(task.output.display_group)

        # add widgets to scroll area:
        layout = task_scroll_widget.layout()
        for group in group_order:

            widget_info = {}

            # add header:
            header = GroupHeader(group, task_scroll_widget)
            layout.addWidget(header)
            widget_info["header"] = header

            # add output items:
            output_widgets = []
            for output in tasks_by_group[group]["outputs"]:
                item = OutputItem(output, task_scroll_widget)
                layout.addWidget(item)
                output_widgets.append(item)
            widget_info["output_widgets"] = output_widgets

            # add item list if more than one item:
            if self.expand_single_items or len(
                    tasks_by_group[group]["items"]) > 1:
                item_list = ItemList(tasks_by_group[group]["items"],
                                     task_scroll_widget)
                layout.addWidget(item_list)
                widget_info["item_list"] = item_list

            # always add error list:
            error_list = ErrorList(tasks_by_group[group]["tasks"],
                                   task_scroll_widget)
            #error_list.setVisible(False)
            layout.addWidget(error_list)
            widget_info["error_list"] = error_list

            self._group_widget_info[group] = widget_info

        # add vertical stretch:
        layout.addStretch(1)

    def _get_selected_tasks(self):
        """
        Get the selected tasks from the UI
        """
        selected_tasks = []

        # build some indexes:
        task_index = {}
        tasks_per_output = {}
        for task in self._tasks:
            key = (task.output, task.item)
            if key in task_index.keys():
                raise "Didn't expect to find the same task in the list twice!"
            task_index[key] = task
            tasks_per_output.setdefault(task.output, list()).append(task)

        for info in self._group_widget_info.values():

            # go through output widgets
            for output_widget in info["output_widgets"]:
                if not output_widget.selected:
                    continue

                output = output_widget.output

                # go through item widgets:
                item_list = info.get("item_list")
                if item_list:
                    for item in item_list.selected_items:
                        task = task_index.get((output, item))
                        if task:
                            selected_tasks.append(task)
                else:
                    # assume all items for this output are selected:
                    tasks = tasks_per_output.get(output)
                    if tasks:
                        selected_tasks.extend(tasks)

        # finally, ensure that tasks are returned in their
        # original order:
        ordered_selected_tasks = [
            task for task in self._tasks if task in selected_tasks
        ]
        return ordered_selected_tasks

    def _on_publish(self):
        self.publish.emit()

    def _on_cancel(self):
        self.cancel.emit()

    def _safe_to_string(self, value):
        """
        safely convert the value to a string - handles
        QtCore.QString if usign PyQt
        """
        #
        if isinstance(value, basestring):
            # it's a string anyway so just return
            return value

        if hasattr(QtCore, "QString"):
            # running PyQt!
            if isinstance(value, QtCore.QString):
                # QtCore.QString inherits from str but supports
                # unicode, go figure!  Lets play safe and return
                # a utf-8 string
                return str(value.toUtf8())

        # For everything else, just return as string
        return str(value)
Ejemplo n.º 30
0
class Worker(QtCore.QThread):
    """
    Background worker class
    """

    # Indicates that this worker class has been fixed to stop
    # gc of QThread from resulting in a crash.  This happens
    # when the mutex object had been gc'd but the thread is
    # still trying to acces it - the fix is to wait for the
    # thread to terminate before returning from 'stop()'
    _SGTK_IMPLEMENTS_QTHREAD_CRASH_FIX_ = True

    work_completed = QtCore.Signal(str, object)
    work_failure = QtCore.Signal(str, str)

    def __init__(self, app, parent=None):
        """
        Construction
        """
        QtCore.QThread.__init__(self, parent)
        self._execute_tasks = True
        self._app = app

        self._queue_mutex = QtCore.QMutex()
        self._queue = []
        self._receivers = {}

        self._wait_condition = QtCore.QWaitCondition()

    def stop(self, wait_for_completion=True):
        """
        Stops the worker, run this before shutdown
        """
        self._execute_tasks = False
        self._wait_condition.wakeAll()
        if wait_for_completion:
            self.wait()

    def clear(self):
        """
        Empties the queue
        """
        self._queue_mutex.lock()
        try:
            self._queue = []
        finally:
            self._queue_mutex.unlock()

    def queue_work(self, worker_fn, params, asap=False):
        """
        Queues up some work.
        Returns a unique identifier to identify this item 
        """
        uid = uuid.uuid4().hex

        work = {"id": uid, "fn": worker_fn, "params": params}
        self._queue_mutex.lock()
        try:
            if asap:
                # first in the queue
                self._queue.insert(0, work)
            else:
                self._queue.append(work)
        finally:
            self._queue_mutex.unlock()

        self._wait_condition.wakeAll()

        return uid

    ############################################################################################
    #

    def run(self):

        while self._execute_tasks:

            # get the next item to process:
            item_to_process = None
            self._queue_mutex.lock()
            try:
                if len(self._queue) == 0:
                    # wait for some more work - this unlocks the mutex
                    # until the wait condition is signalled where it
                    # will then attempt to obtain a lock before returning
                    self._wait_condition.wait(self._queue_mutex)

                    if len(self._queue) == 0:
                        # still nothing in the queue!
                        continue

                item_to_process = self._queue.pop(0)
            finally:
                self._queue_mutex.unlock()

            if not self._execute_tasks:
                break

            # ok, have something to do so lets do it:
            data = None
            try:
                # process the item:
                data = item_to_process["fn"](item_to_process["params"])
            except Exception, e:
                if self._execute_tasks:
                    self.work_failure.emit(item_to_process["id"],
                                           "An error occured: %s" % e)
            else:
                if self._execute_tasks:
                    self.work_completed.emit(item_to_process["id"], data)