def qInitResources():
    QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
Example #2
0
class ProgressDetailsWidget(QtGui.QWidget):
    """
    Progress reporting and logging
    """

    copy_to_clipboard_clicked = QtCore.Signal()

    def __init__(self, parent):
        """
        :param parent: The model parent.
        :type parent: :class:`~PySide.QtGui.QObject`
        """
        super(ProgressDetailsWidget, self).__init__()

        self._bundle = sgtk.platform.current_bundle()

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

        self._filter = None
        self.set_parent(parent)

        # dispatch clipboard signal
        self.ui.copy_log_button.clicked.connect(
            self.copy_to_clipboard_clicked.emit)

        self.ui.close.clicked.connect(self.toggle)

        # make sure the first column takes up as much space as poss.
        if self._bundle.engine.has_qt5:
            # see http://doc.qt.io/qt-5/qheaderview-obsolete.html#setResizeMode
            self.ui.log_tree.header().setSectionResizeMode(
                0, QtGui.QHeaderView.Stretch)
        else:
            self.ui.log_tree.header().setResizeMode(0,
                                                    QtGui.QHeaderView.Stretch)

        self.ui.log_tree.setIndentation(8)

        self.hide()

    def set_parent(self, parent):
        """
        Sets the parent for the progress details widget.
        """

        if self.parent() and self.parent() == parent:
            return

        # remember if the details widget was visible. as per qt docs, we'll need
        # to show it again after reparenting.
        visible = self.isVisible()

        # if we have an existing resize filter, remove it from the current
        # parent widget
        if self._filter:
            self.parent().removeEventFilter(self._filter)

        # reparent the widget
        self.setParent(parent)

        # hook up a new listener to the new parent so this widget follows along
        # when the parent window changes size
        self._filter = ResizeEventFilter(parent)
        self._filter.resized.connect(self._on_parent_resized)
        parent.installEventFilter(self._filter)

        if visible:
            self.show()

    def toggle(self):
        """
        Toggles visibility on and off
        """
        if self.isVisible():
            self.hide()
        else:
            self.show()

    def show(self):
        super(ProgressDetailsWidget, self).show()
        self.__recompute_position()
        self.ui.log_tree.expandAll()

    @property
    def log_tree(self):
        """
        The tree widget which holds the log items
        """
        return self.ui.log_tree

    def __recompute_position(self):
        """
        Adjust geometry of the widget based on progress widget
        """

        # move + fixed size seems to work. setting the geometry is offset for
        # some reason.
        self.move(0, 0)
        self.setFixedWidth(self.parent().width())
        self.setFixedHeight(self.parent().height())

    def _on_parent_resized(self):
        """
        Special slot hooked up to the event filter.
        When associated widget is resized this slot is being called.
        """
        self.__recompute_position()
Example #3
0
class NewProjectForm(QtGui.QWidget):
    """
    The main UI used when creating a new Mari project
    """

    # define signals that this form exposes:
    #
    # emitted when the 'Create Project' button is clicked
    create_project = QtCore.Signal(QtGui.QWidget)
    # emitted when the 'Add Publish' button is clicked
    browse_publishes = QtCore.Signal(QtGui.QWidget)
    # emitted when the user requests to remove publishes from the publish list
    remove_publishes = QtCore.Signal(QtGui.QWidget, list)

    def __init__(self, app, init_proc, initial_name, manager, parent=None):
        """
        Construction
        
        :param app: The current app
        :param init_proc: Called at the end of construction to allow the calling
            code to hook up any signals, etc.
        :param initial_name: The initial name to use in the name field
        :param manager: The active Mari project manager object.
        :param parent: The parent QWidget
        """
        QtGui.QWidget.__init__(self, parent)

        # set up the UI
        from .ui.new_project_form import Ui_NewProjectForm
        self.__ui = Ui_NewProjectForm()
        self.__ui.setupUi(self)

        self.__ui.create_btn.clicked.connect(self._on_create_clicked)
        self.__ui.add_publish_btn.clicked.connect(self._on_add_publish_clicked)
        self.__ui.name_edit.textEdited.connect(self._on_name_edited)

        self.__ui.publish_list.set_app(app)
        self.__ui.publish_list.remove_publishes.connect(
            self._on_remove_selected_publishes)

        self._app = app
        self._manager = manager

        # Fix line colours to match 75% of the text colour.  If we don't do this they are
        # extremely bright compared to all other widgets!  This also seems to be the only
        # way to override the default style sheet?!
        clr = QtGui.QApplication.palette().text().color()
        clr_str = "rgb(%d,%d,%d)" % (clr.red() * 0.75, clr.green() * 0.75,
                                     clr.blue() * 0.75)
        self.__ui.name_line.setStyleSheet("#name_line{color: %s;}" % clr_str)
        self.__ui.publishes_line.setStyleSheet("#publishes_line{color: %s;}" %
                                               clr_str)
        self.__ui.break_line.setStyleSheet("#break_line{color: %s;}" % clr_str)

        # initialise the UI:
        self.__ui.name_edit.setText(initial_name)
        self.update_publishes()
        init_proc(self)

        # Set the initial state of the project name preview.
        self._on_name_edited(self.project_name)

    @property
    def project_name(self):
        """
        Access the entered project name
        :returns: The project name the user entered
        """
        return self.__ui.name_edit.text()

    def update_publishes(self, sg_publish_data=None):
        """
        Update the list of publishes
        
        :param list sg_publish_data: The list of publishes to present. This is a list of 
            Shotgun entity dictionaries.
        """
        # clear the existing publishes from the list:
        self.__ui.publish_list.clear()
        if not sg_publish_data:
            # display the error message in the list and siable the create button:
            self.__ui.publish_list.set_message(
                "<i>You must add at least one publish before "
                "you can create the new project...</i>")
            self.__ui.create_btn.setEnabled(False)
        else:
            # load the publishes into the list and enable the create button:
            self.__ui.publish_list.load(sg_publish_data)
            self.__ui.create_btn.setEnabled(True)

    def closeEvent(self, event):
        """
        Called when the widget is closed so that any cleanup can be 
        done. Overrides QWidget.clostEvent.
        
        :param event: The close event.
        """
        # make sure the publish list BrowserWidget is
        # cleaned up properly
        self.__ui.publish_list.destroy()

        # return result from base implementation
        return QtGui.QWidget.closeEvent(self, event)

    def _on_create_clicked(self):
        """
        Called when the user clicks the create button
        """
        self.create_project.emit(self)

    def _on_add_publish_clicked(self):
        """
        Called when the user clicks the add publish button
        """
        self.browse_publishes.emit(self)

    def _on_name_edited(self, txt):
        """
        Called when the user edits the name
        :param str txt: The current text entered into the edit control
        """
        self._preview_info_updated(
            self._manager._generate_new_project_name(txt))

    def _on_remove_selected_publishes(self, publish_ids):
        """
        Called when the user requests to remove some publishes from the list
        
        :param list publish_ids: The list of publish ids to be removed
        """
        self.remove_publishes.emit(self, publish_ids)

    def _preview_info_updated(self, result):
        """
        Called when the worker thread has finished generating the
        new project name
        
        :param name:    The name entered in to the name edit control
        :param result:  The result returned by the worker thread.  This is a dictionary
                        containing the "project_name" and/or the error "message".
        """
        project_name = result.get("project_name")
        if project_name:
            # updat the preview with the project name:
            self.__ui.name_preview_label.setText("<b>%s</b>" % project_name)
        else:
            # updat the preview with the error message:
            message = result.get("message", "")
            warning = "<p style='color:rgb(226, 146, 0)'>%s</p>" % message
            self.__ui.name_preview_label.setText(warning)
class BackgroundTaskManager(QtCore.QObject):
    """
    Main task manager class. Manages a queue of tasks running them asynchronously through
    a pool of worker threads.

    The BackgroundTaskManager class itself is reentrant but not thread-safe so its methods should only
    be called from the thread it is created in. Typically this would be the main thread of the application.

    :signal task_completed(uid, group, result): Emitted when a task has been completed.
        The ``uid`` parameter holds the unique id associated with the task,
        the ``group`` is the group that the task is associated with and
        the ``result`` is the data returned by the task.

    :signal task_failed(uid, group, message, traceback_str): Emitted when a task fails for some reason.
        The ``uid`` parameter holds the unique id associated with the task,
        the ``group`` is the group that the task is associated with,
        the ``message`` is a short error message and the ``traceback_str``
        holds a full traceback.

    :signal task_group_finished(group): Emitted when all tasks in a group have finished.
        The ``group`` is the group that has completed.

    """

    # signal emitted when a task has been completed
    task_completed = QtCore.Signal(int, object, object)# uid, group, result
    # signal emitted when a task fails for some reason
    task_failed = QtCore.Signal(int, object, str, str)# uid, group, msg, traceback
    # signal emitted when all tasks in a group have finished
    task_group_finished = QtCore.Signal(object)# group

    def __init__(self, parent, start_processing=False, max_threads=8):
        """
        :param parent:              The parent QObject for this instance
        :type parent:               :class:`~PySide.QtGui.QWidget`
        :param start_processing:    If True then processing of tasks will start immediately
        :param max_threads:         The maximum number of threads the task manager will use at any
                                    time.
        """
        QtCore.QObject.__init__(self, parent)

        self._bundle = sgtk.platform.current_bundle()

        self._next_task_id = 0
        self._next_group_id = 0

        self._can_process_tasks = start_processing

        # the pending tasks, organized by priority
        self._pending_tasks_by_priority = {}

        # available threads and running tasks:
        self._max_threads = max_threads or 8
        self._all_threads = []
        self._available_threads = []
        self._running_tasks = {}

        # various task look-up maps:
        self._tasks_by_id = {}
        self._group_task_map = {}

        # track downstream dependencies for tasks:
        self._upstream_task_map = {}
        self._downstream_task_map = {}

        # Create the results dispatcher
        self._results_dispatcher = ResultsDispatcher(self)
        self._results_dispatcher.task_completed.connect(self._on_worker_thread_task_completed)
        self._results_dispatcher.task_failed.connect(self._on_worker_thread_task_failed)
        self._results_dispatcher.start()

    def next_group_id(self):
        """
        Return the next available group id

        :returns:    A unique group id to be used for tasks that belong to the same group.
        """
        group_id = self._next_group_id
        self._next_group_id += 1
        return group_id

    def _low_level_debug_log(self, msg):
        """
        Wrapper method for logging *detailed* info
        to debug. This is disabled by default but can
        be useful to enable for example when debugging
        issues around concurrency. In order to enable it,
        set the ENABLE_DETAILED_DEBUG constant at the top
        of this file to True.

        :param msg: The message to be logged.
        """
        if ENABLE_DETAILED_DEBUG:
            self._debug_log(msg)

    def _debug_log(self, msg):
        """
        Wrapper method for logging useful information to debug.

        :param msg: The message to be logged.
        """
        self._bundle.log_debug("Task Manager: %s" % msg)

    def start_processing(self):
        """
        Start processing of tasks
        """
        self._can_process_tasks = True
        self._start_tasks()

    def pause_processing(self):
        """
        Pause processing of tasks - any currently running tasks will
        complete as normal.
        """
        self._can_process_tasks = False
        # and just let the current threads complete...

    def shut_down(self):
        """
        Shut down the task manager.  This clears the task queue and gracefully stops all running
        threads.  Completion/failure of any currently running tasks will be ignored.
        """
        self._debug_log("Shutting down...")
        self._can_process_tasks = False

        # stop all tasks:
        self.stop_all_tasks()

        # shut down all worker threads:
        self._debug_log("Waiting for %d background threads to stop..." % len(self._all_threads))
        for thread in self._all_threads:
            thread.shut_down()
        self._available_threads = []
        self._all_threads = []

        # Shut down the dispatcher thread
        self._results_dispatcher.shut_down()
        self._debug_log("Shut down successfully!")

    def add_task(self, cbl, priority=None, group=None, upstream_task_ids=None, task_args=None, task_kwargs=None):
        """
        Add a new task to the queue.  A task is a callable method/class together with any arguments that
        should be passed to the callable when it is called.

        :param cbl:                 The callable function/class to call when executing the task
        :param priority:            The priority this task should be run with.  Tasks with higher priority
                                    are run first.
        :param group:               The group this task belongs to.  Task groups can be used to simplify task
                                    management (e.g. stop a whole group, be notified when a group is complete)
        :param upstream_task_ids:   A list of any upstream tasks that should be completed before this task
                                    is run.  The results from any upstream tasks are appended to the kwargs
                                    for this task.
        :param task_args:           A list of unnamed parameters to be passed to the callable when running the
                                    task
        :param task_kwargs:         A dictionary of named parameters to be passed to the callable when running
                                    the task
        :returns:                   A unique id representing the task.
        """
        if not callable(cbl):
            raise TankError("The task function, method or object '%s' must be callable!" % cbl)

        upstream_task_ids = set(upstream_task_ids or [])

        # create a new task instance:
        task_id = self._next_task_id
        self._next_task_id += 1
        new_task = BackgroundTask(task_id, cbl, group, priority, task_args, task_kwargs)

        # add the task to the pending queue:
        self._pending_tasks_by_priority.setdefault(priority, []).append(new_task)

        # add tasks to various look-ups:
        self._tasks_by_id[new_task.uid] = new_task
        self._group_task_map.setdefault(group, set()).add(new_task.uid)

        # keep track of the task dependencies:
        self._upstream_task_map[new_task.uid] = upstream_task_ids
        for us_task_id in upstream_task_ids:
            self._downstream_task_map.setdefault(us_task_id, set()).add(new_task.uid)

        self._low_level_debug_log("Added Task %s to the queue" % new_task)

        # and start the next task:
        self._start_tasks()

        return new_task.uid

    def add_pass_through_task(self, priority=None, group=None, upstream_task_ids=None, task_kwargs=None):
        """
        Add a pass-through task to the queue.  A pass-through task doesn't perform any work but can be useful
        when synchronising other tasks (e.g. pulling the results from multiple upstream tasks into a single task)

        :param priority:            The priority this task should be run with.  Tasks with higher priority
                                    are run first.
        :param group:               The group this task belongs to.  Task groups can be used to simplify task
                                    management (e.g. stop a whole group, be notified when a group is complete).
                                    A group is expressed as a string, for example 'thumbnails', 'IO' or 'shotgun'.
        :param upstream_task_ids:   A list of any upstream tasks that should be completed before this task
                                    is run.  The results from any upstream tasks are appended to the kwargs
                                    for this task.
        :param task_kwargs:         A dictionary of named parameters that will be appended to the result of
                                    the pass-through task.
        :returns:                   A unique id representing the task.

        """
        return self.add_task(self._task_pass_through, priority, group, upstream_task_ids, task_kwargs=task_kwargs)

    def stop_task(self, task_id, stop_upstream=True, stop_downstream=True):
        """
        Stop the specified task from running.  If the task is already running then it will complete but
        the completion/failure signal will be ignored.

        :param task_id:         The id of the task to stop
        :param stop_upstream:   If true then all upstream tasks will also be stopped
        :param stop_downstream: If true then all downstream tasks will also be stopped
        """
        task = self._tasks_by_id.get(task_id)
        if task is None:
            return

        self._low_level_debug_log("Stopping Task %s..." % task)
        self._stop_tasks([task], stop_upstream, stop_downstream)
        self._low_level_debug_log(" > Task %s stopped!" % task)

    def stop_task_group(self, group, stop_upstream=True, stop_downstream=True):
        """
        Stop all tasks in the specified group from running.  If any tasks are already running then they will
        complete but their completion/failure signals will be ignored.

        :param group:           The task group to stop
        :param stop_upstream:   If true then all upstream tasks will also be stopped
        :param stop_downstream: If true then all downstream tasks will also be stopped
        """
        task_ids = self._group_task_map.get(group)
        if task_ids is None:
            return

        self._low_level_debug_log("Stopping Task group %s..." % group)

        tasks_to_stop = []
        for task_id in task_ids:
            task = self._tasks_by_id.get(task_id)
            if task:
                tasks_to_stop.append(task)
        del self._group_task_map[group]
        self._stop_tasks(tasks_to_stop, stop_upstream, stop_downstream)

        self._low_level_debug_log(" > Task group %s stopped!" % group)

    def stop_all_tasks(self):
        """
        Stop all currently queued or running tasks.  If any tasks are already running then they will
        complete but their completion/failure signals will be ignored.
        """
        self._debug_log("Stopping all tasks...")

        # we just need to clear all the lookups:
        self._running_tasks = {}
        self._pending_tasks_by_priority = {}
        self._tasks_by_id = {}
        self._group_task_map = {}
        self._upstream_task_map = {}
        self._downstream_task_map = {}

        self._debug_log(" > All tasks stopped!")

    def _stop_tasks(self, tasks_to_stop, stop_upstream, stop_downstream):
        """
        Stop the specified list of tasks

        :param tasks_to_stop:   A list of tasks to stop
        :param stop_upstream:   If true then all upstream tasks will also be stopped
        :param stop_downstream: If true then all downstream tasks will also be stopped
        """
        if not tasks_to_stop:
            return

        # copy the task list as we'll be modifying it:
        tasks_to_stop = list(tasks_to_stop)
        # and make sure we only stop each task once!!
        stopped_task_ids = set([task.uid for task in tasks_to_stop])

        while tasks_to_stop:
            task_to_stop = tasks_to_stop.pop(0)

            # get the up & downstream tasks to also stop depending on the flags:
            if stop_upstream and task_to_stop.uid in self._upstream_task_map:
                for us_task_id in self._upstream_task_map[task_to_stop.uid]:
                    us_task = self._tasks_by_id.get(us_task_id)
                    if not us_task or us_task.uid in stopped_task_ids:
                        # no task or already found
                        continue

                    tasks_to_stop.append(us_task)
                    stopped_task_ids.add(us_task_id)

            if stop_downstream and task_to_stop.uid in self._downstream_task_map:
                for ds_task_id in self._downstream_task_map[task_to_stop.uid]:
                    ds_task = self._tasks_by_id.get(ds_task_id)
                    if not ds_task or ds_task.uid in stopped_task_ids:
                        # no task or already found
                        continue

                    tasks_to_stop.append(ds_task)
                    stopped_task_ids.add(ds_task_id)

            # remove the task:
            self._remove_task(task_to_stop)

    def _get_worker_thread(self):
        """
        Get a worker thread to use.

        :returns:   An available worker thread if there is one, a new thread if needed or None if the thread
                    limit has been reached.
        """
        if self._available_threads:
            # we can just use one of the available threads:
            return self._available_threads.pop()

        # no available threads so lets check our thread count:
        thread_count = len(self._all_threads)
        if thread_count >= self._max_threads:
            # no available threads left!
            return None

        # create a new worker thread - note, there are two different implementations of the WorkerThread class
        # that use two different recipes.  Although WorkerThreadB is arguably more correct it has some issues
        # in PyQt so WorkerThread is currently preferred - see the notes in the class above for further details
        thread = WorkerThread(self._results_dispatcher, self)
        if not isinstance(thread, WorkerThread):
            # for some reason (probably memory corruption somewhere else) I've occasionally seen the above
            # creation of a worker thread return another arbitrary object!  Added this in here so the code
            # will at least continue correctly and not do unexpected things!
            self._bundle.log_error("Failed to create background worker thread for task Manager!")
            return None
        self._all_threads.append(thread)

        # start the thread - this will just put it into wait mode:
        thread.start()

        # log some debug:
        self._debug_log("Started new background worker thread (num threads=%d)" % len(self._all_threads))

        return thread

    def _start_tasks(self):
        """
        Start any queued tasks that are startable if there are available threads to run them.
        """
        # start tasks until we fail to start one for whatever reason:
        started = True
        while started:
            started = self._start_next_task()

    def _start_next_task(self):
        """
        Start the next task in the queue if there is a task that is startable and there is an
        available thread to run it.

        :returns:    True if a task was started, otherwise False
        """
        if not self._can_process_tasks:
            return False

        # figure out next task to start from the priority queue:
        task_to_process = None
        priorities = sorted(self._pending_tasks_by_priority.keys(), reverse=True)
        for priority in priorities:
            # iterate through the tasks and make sure we aren't waiting on the
            # completion of any upstream tasks:
            for task in self._pending_tasks_by_priority[priority]:
                awaiting_upstream_task_completion = False
                for us_task_id in self._upstream_task_map.get(task.uid, []):
                    if us_task_id in self._tasks_by_id:
                        # if the task is still in the tasks list then we're still awaiting
                        # completion of it!
                        awaiting_upstream_task_completion = True
                        break
                if awaiting_upstream_task_completion:
                    continue

                # ok, we've found the next task to process:
                task_to_process = task
                break

            if task_to_process:
                # no need to look any further!
                break

        if not task_to_process:
            # nothing to do!
            return False

        # we need a thread to do the work with:
        thread = self._get_worker_thread()
        if not thread:
            # looks like we can't do anything!
            return False

        self._low_level_debug_log("Starting task %r" % task_to_process)

        # ok, we have a thread so lets move the task from the priority queue to the running list:
        self._pending_tasks_by_priority[priority].remove(task_to_process)
        if not self._pending_tasks_by_priority[priority]:
            # no more tasks with this priority so also clean up the list
            del self._pending_tasks_by_priority[priority]
        self._running_tasks[task_to_process.uid] = (task_to_process, thread)

        num_tasks_left = 0
        for pending_tasks in self._pending_tasks_by_priority.itervalues():
            num_tasks_left += len(pending_tasks)

        self._low_level_debug_log(
            " > Currently running tasks: '%s' - %d left in queue" % (self._running_tasks.keys(), num_tasks_left)
        )

        # and run the task
        thread.run_task(task_to_process)

        return True

    def _on_worker_thread_task_completed(self, worker_thread, task, result):
        """
        Slot triggered when a task is completed by a worker thread.  This processes the result and emits the
        task_completed signal if needed.

        :param worker_thread: Thread that completed the task.
        :param task:          The task that completed
        :param result:        The task result
        """
        try:
            # check that we should process this result:
            if task.uid in self._running_tasks:

                self._low_level_debug_log("Task %r - completed" % (task))

                # if we have dependent tasks then update them:
                for ds_task_id in self._downstream_task_map.get(task.uid, []):
                    ds_task = self._tasks_by_id.get(ds_task_id)
                    if not ds_task:
                        continue

                    # update downstream task with result
                    ds_task.append_upstream_result(result)

                # remove the task:
                group_finished = self._remove_task(task)

                # emit signal that this task is completed:
                self.task_completed.emit(task.uid, task.group, result)

                if group_finished:
                    # also emit signal that the entire group is completed:
                    self.task_group_finished.emit(task.group)
        finally:
            # move this task thread to the available threads list:
            self._available_threads.append(worker_thread)

        # start processing of the next task:
        self._start_tasks()

    def _on_worker_thread_task_failed(self, worker_thread, task, msg, tb):
        """
        Slot triggered when a task being executed in by a worker thread has failed for some reason.  This processes
        the task and emits the task_failed signal if needed.

        :param worker_thread: Thread that completed the task.
        :param task:          The task that failed
        :param msg:           The error message for the failed task
        :param tb:            The stack-trace for the failed task
        """
        try:
            # check that we should process this task:
            if task.uid in self._running_tasks:
                self._debug_log("Task %r - failed: %s\n%s" % (task, msg, tb))

                # we need to emit the failed message for this task as well as any that have
                # upstream dependencies on this task!
                failed_tasks = [task]
                failed_task_ids = set([task.uid])
                finished_groups = set()
                while failed_tasks:
                    failed_task = failed_tasks.pop(0)

                    # find any downstream tasks:
                    for ds_task_id in self._downstream_task_map.get(failed_task.uid) or []:
                        ds_task = self._tasks_by_id.get(ds_task_id)
                        if not ds_task or ds_task.uid in failed_task_ids:
                            # no task or already found
                            continue
                        failed_tasks.append(ds_task)
                        failed_task_ids.add(ds_task.uid)

                    # remove the task:
                    group_finished = self._remove_task(failed_task)

                    # emit failed signal for the failed task:
                    self.task_failed.emit(failed_task.uid, failed_task.group, msg, tb)

                    if group_finished and failed_task.group not in finished_groups:
                        self.task_group_finished.emit(failed_task.group)
                        finished_groups.add(failed_task.group)
        finally:
            # move this task thread to the available threads list:
            self._available_threads.append(worker_thread)

        # start processing of the next task:
        self._start_tasks()

    def _remove_task(self, task):
        """
        Remove the specified task from the queue.

        :param task:    The task to remove from the queue
        """
        group_completed = False

        # fist remove from the running tasks - this will stop any signals being handled for this task
        if task.uid in self._running_tasks:
            del self._running_tasks[task.uid]

        # find and remove the task from the pending queue:
        if task.priority in self._pending_tasks_by_priority:
            for p_task in self._pending_tasks_by_priority.get(task.priority, []):
                if p_task.uid == task.uid:
                    self._pending_tasks_by_priority[task.priority].remove(p_task)
                    break

            if not self._pending_tasks_by_priority[task.priority]:
                del self._pending_tasks_by_priority[task.priority]

        # remove this task from all other maps:
        if (task.group in self._group_task_map and
                task.uid in self._group_task_map[task.group]):
            self._group_task_map[task.group].remove(task.uid)
            if not self._group_task_map[task.group]:
                group_completed = True
                del self._group_task_map[task.group]
        if task.uid in self._tasks_by_id:
            del self._tasks_by_id[task.uid]
        if task.uid in self._upstream_task_map:
            del self._upstream_task_map[task.uid]
        if task.uid in self._downstream_task_map:
            del self._downstream_task_map[task.uid]

        return group_completed

    def _task_pass_through(self, **kwargs):
        """
        Pass-through task callable.  Simply returns the input kwargs as the result

        :param **kwargs:    The named arguments for the task
        :returns:           A dictionary containing the named input arguments.
        """
        return kwargs
class HierarchicalSearchCompleter(SearchCompleter):
    """
    A standalone :class:`PySide.QtGui.QCompleter` class for matching SG entities to typed text.

    If defaults to searching inside the current context's project and to only show entities.

    :signal: ``node_activated(str, int, str, str, list)`` - Fires when someone activates a
        node inside the search results. The parameters are ``type``, ``id``, ``name``,
        ``label_path`` and ``incremental_path``. If the node activated is not an entity,
        ``type`` and ``id`` will be ``None``.

    :modes: ``MODE_LOADING, MODE_NOT_FOUND, MODE_RESULT`` - Used to identify the
        mode of an item in the completion list

    :model role: ``MODE_ROLE`` - Stores the mode of an item in the completion
        list (see modes above)

    :model role: ``SG_DATA_ROLE`` - Role for storing shotgun data in the model
    """

    # path label, entity type, entity id, name, incremental path
    node_activated = QtCore.Signal(str, int, str, str, list)

    def __init__(self, parent=None):
        """
        :param parent: Parent widget
        :type parent: :class:`~PySide.QtGui.QWidget`
        """
        super(HierarchicalSearchCompleter, self).__init__(parent)
        self.search_root = self._bundle.context.project
        self.show_entities_only = True
        self.seed_entity_field = "PublishedFile.entity"

    def _get_search_root(self):
        """
        The entity under which the search will be done. If ``None``, the search will be done
        for the whole site.

        The entity is a ``dict`` with keys ``id`` and ``type``. Note that only ``Project`` entities
        are supported at the moment.
        """
        return self._search_root

    def _set_search_root(self, entity):
        """
        See getter documentation.
        """
        self._search_root = entity

    search_root = property(_get_search_root, _set_search_root)

    def _get_show_entities_only(self):
        """
        Indicates if only entities will be shown in the search results.

        If set to ``True``, only entities will be shown.
        """
        return self._show_entities_only

    def _set_show_entities_only(self, is_set):
        """
        See getter documentation.
        """
        self._show_entities_only = is_set

    show_entities_only = property(_get_show_entities_only,
                                  _set_show_entities_only)

    def _get_seed_entity_field(self):
        """
        The seed entity to use when searching for entity.

        Can be ``PublishedFile.entity`` or ``Version.entity``.
        """
        return self._seed_entity_field

    def _set_seed_entity_field(self, seed_entity_field):
        """
        See setter documentation.
        """
        self._seed_entity_field = seed_entity_field

    seed_entity_field = property(_get_seed_entity_field,
                                 _set_seed_entity_field)

    def _set_item_delegate(self, popup, text):
        """
        Sets the item delegate for the popup widget.

        :param popup: Qt Popup widget receiving the delegate.
        :paarm text: Text from the current search.
        """
        # deferred import to help documentation generation.
        from .hierarchical_search_result_delegate import (
            HierarchicalSearchResultDelegate, )

        self._delegate = HierarchicalSearchResultDelegate(popup, text)
        popup.setItemDelegate(self._delegate)

    def _launch_sg_search(self, text):
        """
        Launches a search on the Shotgun server.

        :param str text: Text to search for.

        :returns: The :class:`~tk-framework-shotgunutils:shotgun_data.ShotgunDataRetriever`'s job id.
        """
        # FIXME: Ideally we would use the nav_search_entity endpoint to compute the path to the root.
        # Unfortunately there is a bug a the moment that prevents this.
        if not self._search_root:
            root_path = "/"
        else:
            root_path = "/Project/%d" % self._search_root.get("id")

        return self._sg_data_retriever.execute_nav_search_string(
            root_path, text, self._seed_entity_field)

    def _handle_search_results(self, data):
        """
        Populates the model associated with the completer with the data coming back from Shotgun.

        :param dict data: Data received back from the job sent to the
            :class:`~tk-framework-shotgunutils:shotgun_data.ShotgunDataRetriever` in :method:``_launch_sg_search``.
        """
        data_matches = data["sg"]

        # When showing only entities, skip entries that aren't.
        if self._show_entities_only:
            data_matches = filter(
                lambda x: x["ref"]["type"] is not None and x["ref"]["id"] is
                not None,
                data_matches,
            )

        if len(data_matches) == 0:
            item = QtGui.QStandardItem("No matches found!")
            item.setData(self.MODE_NOT_FOUND, self.MODE_ROLE)
            self.model().appendRow(item)

        # Payload looks like:
        # [
        #     {
        #         "label": "bunny_020",
        #         "incremental_path": [
        #             "/Project/65",
        #             "/Project/65/Shot",
        #             "/Project/65/Shot/sg_sequence/Sequence/5"
        #         ],
        #         "path_label": "Shots",
        #         "ref": {
        #             "id": 5,
        #             "type": "Sequence"
        #         },
        #         "project_id": 65
        #     },
        #     ...
        # ]

        # insert new data into model
        for data in data_matches:

            item = QtGui.QStandardItem(data["label"])
            item.setData(self.MODE_RESULT, self.MODE_ROLE)

            item.setData(shotgun_model.sanitize_for_qt_model(data),
                         self.SG_DATA_ROLE)

            item.setIcon(self._pixmaps.no_thumbnail)

            data_type = data["ref"]["type"]
            data_id = data["ref"]["id"]
            if data_type and data_id and self._sg_data_retriever:
                uid = self._sg_data_retriever.request_thumbnail_source(
                    data_type, data_id, load_image=True)
                self._thumb_map[uid] = {"item": item}

            self.model().appendRow(item)

    def get_result(self, model_index):
        """
        Returns an item from the result list.

        Here's an example::

            {
                "label": "bunny_020",
                "incremental_path": [
                    "/Project/65",
                    "/Project/65/Shot",
                    "/Project/65/Shot/sg_sequence/Sequence/5"
                ],
                "path_label": "Shots",
                "ref": {
                    "id": 5,
                    "type": "Sequence"
                },
                "project_id": 65
            }

        :param model_index: The index of the model to return the result for.
        :type model_index: :class:`~PySide.QtCore.QModelIndex`

        :return: The ``dict`` for the supplied model index.
        :rtype: ``dict`` or ``None``
        """
        mode = shotgun_model.get_sanitized_data(model_index, self.MODE_ROLE)
        if mode == self.MODE_RESULT:
            # get the payload
            data = shotgun_model.get_sanitized_data(model_index,
                                                    self.SG_DATA_ROLE)
            return data
        else:
            return None

    def _on_select(self, model_index):
        """
        Called by the base class when something was selected in the pop-up. Emits
        the ``node_activated`` event.

        :param model_index: :class:`QtModelIndex` of the item that was selected.
        """
        data = self.get_result(model_index)
        if data:
            # Let it be known that something was picked.
            self.node_activated.emit(
                data["ref"]["type"],
                data["ref"]["id"],
                data["label"],
                data["path_label"],
                data["incremental_path"],
            )
Example #6
0
    def paint(self, painter, style_options, model_index):
        """
        Paint method to handle all cells that are not being currently edited.

        :param painter:         The painter instance to use when painting
        :param style_options:   The style options to use when painting
        :param model_index:     The index in the data model that needs to be painted
        """
        sg_item = shotgun_model.get_sg_data(model_index)

        original_tn = model_index.data(self._ORIGINAL_THUMBNAIL)
        pinned_tn = model_index.data(self._PINNED_THUMBNAIL)
        filter_tn = model_index.data(self._FILTER_THUMBNAIL)

        # for performance reasons, we are not creating a widget every time
        # but merely moving the same widget around.
        paint_widget = self._get_painter_widget(model_index, self.parent())
        if not paint_widget:
            # just paint using the base implementation:
            QtGui.QStyledItemDelegate.paint(self, painter, style_options,
                                            model_index)
            return

        # make sure that the widget that is just used for painting isn't visible otherwise
        # it'll appear in the wrong place!
        paint_widget.setVisible(False)

        # call out to have the widget set the right values
        self._on_before_paint(paint_widget, model_index, style_options)

        # now paint!
        painter.save()
        try:
            paint_widget.resize(style_options.rect.size())
            painter.translate(style_options.rect.topLeft())
            # note that we set the render flags NOT to render the background of the widget
            # this makes it consistent with the way the editor widget is mounted inside
            # each element upon hover.

            paint_widget.render(painter,
                                QtCore.QPoint(0, 0),
                                renderFlags=QtGui.QWidget.DrawChildren)

            if self.tray_view.rv_mode.index_is_pinned(model_index.row()):
                painter.drawPixmap(
                    paint_widget.width() - self.pin_pixmap.width(), 0,
                    self.pin_pixmap)

            if not sg_item.get('version.Version.id') and not sg_item.get(
                    'image') and not sg_item.get(
                        'cut.Cut.version.Version.image'):
                target = QtCore.QRectF(0.0, 0.0, paint_widget.width(),
                                       paint_widget.height())
                source = QtCore.QRectF(0, 0, self.missing_pixmap.width(),
                                       self.missing_pixmap.height())
                # painter.drawPixmap(target, self.missing_pixmap, source)
                painter.fillRect(0, 0, paint_widget.width(),
                                 paint_widget.height(),
                                 QtGui.QColor(10, 0, 0, 255))
                painter.drawText(
                    0, 5, 100, 100,
                    QtCore.Qt.AlignHCenter | QtCore.Qt.AlignCenter, 'MISSING')

            mini_data = self.tray_view.rv_mode.cached_mini_cut_data

            if mini_data.active and painter:
                if mini_data.focus_clip == model_index.row():
                    painter.setPen(self._pen)
                    ws = paint_widget.size()
                    painter.drawRect(1, 1, ws.width() - 2, ws.height() - 2)

                if mini_data.first_clip > model_index.row(
                ) or mini_data.last_clip < model_index.row():
                    painter.fillRect(0, 0, paint_widget.width(),
                                     paint_widget.height(),
                                     QtGui.QColor(0, 0, 0, 220))

        finally:
            painter.restore()
class SceneBrowserWidget(browser_widget.BrowserWidget):

    _item_work_completed = QtCore.Signal(str, object)
    _item_work_failed = QtCore.Signal(str, str)

    def __init__(self, parent=None):
        browser_widget.BrowserWidget.__init__(self, parent)

    def get_data(self, data):
        items = breakdown.get_breakdown_items()
        return {
            "items": items,
            "show_red": data["show_red"],
            "show_green": data["show_green"],
        }

    def _make_row(self, first, second):
        return "<tr><td><b>%s</b>&nbsp;&nbsp;&nbsp;</td><td>%s</td></tr>" % (
            first,
            second,
        )

    def set_app(self, app):
        browser_widget.BrowserWidget.set_app(self, app)
        # For some reason connecting the worker signal directly to the child items' slots causes a
        # segmentation fault when there are many items. But re-emitting a local signal works fine
        self._worker.notifier.work_completed.connect(
            lambda uid, data: self._item_work_completed.emit(uid, data)
        )
        self._worker.notifier.work_failure.connect(
            lambda uid, msg: self._item_work_failed.emit(uid, msg)
        )

    def process_result(self, result):

        if len(result.get("items")) == 0:
            self.set_message("No versioned data in your scene!")
            return

        ################################################################################
        # PASS 1 - grouping
        # group these items into various buckets first based on type, and asset type
        groups = {}

        for d in result["items"]:

            if d.get("sg_data"):

                # publish in shotgun!
                sg_data = d["sg_data"]

                entity = sg_data.get("entity")
                if entity is None:
                    entity_type = "Unknown Type"
                else:
                    entity_type = shotgun_globals.get_type_display_name(entity["type"])

                asset_type = sg_data["entity.Asset.sg_asset_type"]

                if asset_type:
                    group = "%ss" % asset_type  # eg. Characters
                else:
                    group = "%ss" % entity_type  # eg. Shots

                # it is an asset, so group by asset type
                if group not in groups:
                    groups[group] = []
                groups[group].append(d)

            else:
                # everything not in shotgun goes into the other bucket
                OTHER_ITEMS = "Unpublished Items"
                if OTHER_ITEMS not in groups:
                    groups[OTHER_ITEMS] = []
                groups[OTHER_ITEMS].append(d)

        ################################################################################
        # PASS 2 - display the content of all groups

        if sgtk.util.get_published_file_entity_type(self._app.sgtk) == "PublishedFile":
            published_file_type_field = "published_file_type"
        else:  # == "TankPublishedFile"
            published_file_type_field = "tank_type"

        # now iterate through the groups
        for group in sorted(groups.keys()):

            i = self.add_item(browser_widget.ListHeader)
            i.set_title(group)
            for d in groups[group]:

                # item has a publish in sg
                i = self.add_item(BreakdownListItem)

                # provide a limited amount of data for receivers via the
                # data dictionary on
                # the item object
                i.data = {
                    "node_name": d["node_name"],
                    "node_type": d["node_type"],
                    "template": d["template"],
                    "fields": d["fields"],
                }

                # populate the description
                details = []

                if d.get("sg_data"):

                    sg_data = d["sg_data"]

                    details.append(
                        self._make_row(
                            "Item",
                            "%s, Version %d"
                            % (sg_data["name"], sg_data["version_number"]),
                        )
                    )

                    # see if this publish is associated with an entity
                    linked_entity = sg_data.get("entity")
                    if linked_entity:
                        display_name = shotgun_globals.get_type_display_name(
                            linked_entity["type"]
                        )

                        details.append(
                            self._make_row(display_name, linked_entity["name"])
                        )

                    # does it have a tank type ?
                    if sg_data.get(published_file_type_field):
                        details.append(
                            self._make_row(
                                "Type",
                                sg_data.get(published_file_type_field).get("name"),
                            )
                        )

                    details.append(self._make_row("Node", d["node_name"]))

                else:

                    details.append(self._make_row("Version", d["fields"]["version"]))

                    # display some key fields in the widget
                    # todo: make this more generic?
                    relevant_fields = ["Shot", "Asset", "Step", "Sequence", "name"]

                    for (k, v) in d["fields"].items():
                        # only show relevant fields - a bit of a hack
                        if k in relevant_fields:
                            details.append(self._make_row(k, v))

                    details.append(self._make_row("Node", d["node_name"]))

                inner = "".join(details)

                i.set_details("<table>%s</table>" % inner)

                # finally, ask the node to calculate its red-green status
                # this will happen asynchronously.
                i.calculate_status(
                    d["template"],
                    d["fields"],
                    result["show_red"],
                    result["show_green"],
                    d.get("sg_data"),
                )
Example #8
0
    def setupUi(self, NewTaskForm):
        NewTaskForm.setObjectName("NewTaskForm")
        NewTaskForm.resize(380, 270)
        NewTaskForm.setMinimumSize(QtCore.QSize(380, 270))
        self.verticalLayout = QtGui.QVBoxLayout(NewTaskForm)
        self.verticalLayout.setSpacing(4)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.verticalLayout_2 = QtGui.QVBoxLayout()
        self.verticalLayout_2.setSpacing(20)
        self.verticalLayout_2.setContentsMargins(12, 12, 12, 4)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.label_3 = QtGui.QLabel(NewTaskForm)
        self.label_3.setWordWrap(True)
        self.label_3.setObjectName("label_3")
        self.verticalLayout_2.addWidget(self.label_3)
        self.gridLayout = QtGui.QGridLayout()
        self.gridLayout.setHorizontalSpacing(20)
        self.gridLayout.setVerticalSpacing(6)
        self.gridLayout.setObjectName("gridLayout")
        self.assigned_to = QtGui.QLabel(NewTaskForm)
        self.assigned_to.setObjectName("assigned_to")
        self.gridLayout.addWidget(self.assigned_to, 7, 2, 1, 1)
        self.label_6 = QtGui.QLabel(NewTaskForm)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label_6.setFont(font)
        self.label_6.setObjectName("label_6")
        self.gridLayout.addWidget(self.label_6, 7, 0, 1, 1)
        self.label_4 = QtGui.QLabel(NewTaskForm)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label_4.setFont(font)
        self.label_4.setObjectName("label_4")
        self.gridLayout.addWidget(self.label_4, 8, 0, 1, 1)
        self.pipeline_step = QtGui.QComboBox(NewTaskForm)
        self.pipeline_step.setObjectName("pipeline_step")
        self.gridLayout.addWidget(self.pipeline_step, 1, 2, 1, 1)
        spacerItem = QtGui.QSpacerItem(10, 10, QtGui.QSizePolicy.Minimum,
                                       QtGui.QSizePolicy.Fixed)
        self.gridLayout.addItem(spacerItem, 6, 0, 1, 1)
        self.entity = QtGui.QLabel(NewTaskForm)
        self.entity.setObjectName("entity")
        self.gridLayout.addWidget(self.entity, 8, 2, 1, 1)
        self.label = QtGui.QLabel(NewTaskForm)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.gridLayout.addWidget(self.label, 1, 0, 1, 1)
        self.label_2 = QtGui.QLabel(NewTaskForm)
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.label_2.setFont(font)
        self.label_2.setObjectName("label_2")
        self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1)
        self.task_name = QtGui.QLineEdit(NewTaskForm)
        self.task_name.setObjectName("task_name")
        self.gridLayout.addWidget(self.task_name, 0, 2, 1, 1)
        self.gridLayout.setColumnStretch(2, 1)
        self.verticalLayout_2.addLayout(self.gridLayout)
        self.warning = QtGui.QLabel(NewTaskForm)
        self.warning.setText("")
        self.warning.setWordWrap(True)
        self.warning.setObjectName("warning")
        self.verticalLayout_2.addWidget(self.warning)
        self.verticalLayout.addLayout(self.verticalLayout_2)
        spacerItem1 = QtGui.QSpacerItem(20, 11, QtGui.QSizePolicy.Minimum,
                                        QtGui.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem1)
        self.break_line = QtGui.QFrame(NewTaskForm)
        self.break_line.setFrameShape(QtGui.QFrame.HLine)
        self.break_line.setFrameShadow(QtGui.QFrame.Sunken)
        self.break_line.setObjectName("break_line")
        self.verticalLayout.addWidget(self.break_line)
        self.horizontalLayout = QtGui.QHBoxLayout()
        self.horizontalLayout.setSizeConstraint(
            QtGui.QLayout.SetDefaultConstraint)
        self.horizontalLayout.setContentsMargins(12, 8, 12, 12)
        self.horizontalLayout.setObjectName("horizontalLayout")
        spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem2)
        self.cancel_btn = QtGui.QPushButton(NewTaskForm)
        self.cancel_btn.setObjectName("cancel_btn")
        self.horizontalLayout.addWidget(self.cancel_btn)
        self.create_btn = QtGui.QPushButton(NewTaskForm)
        self.create_btn.setDefault(True)
        self.create_btn.setObjectName("create_btn")
        self.horizontalLayout.addWidget(self.create_btn)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.verticalLayout.setStretch(2, 1)

        self.retranslateUi(NewTaskForm)
        QtCore.QObject.connect(self.cancel_btn, QtCore.SIGNAL("clicked()"),
                               NewTaskForm.close)
        QtCore.QMetaObject.connectSlotsByName(NewTaskForm)
        NewTaskForm.setTabOrder(self.task_name, self.pipeline_step)
        NewTaskForm.setTabOrder(self.pipeline_step, self.create_btn)
        NewTaskForm.setTabOrder(self.create_btn, self.cancel_btn)
Example #9
0
class FileFilters(QtCore.QObject):
    """
    Implementation of the FileFilters class
    """

    # signal emitted whenever something in the filters is changed
    changed = QtCore.Signal()
    # signal emitted whenever the available users are changed
    available_users_changed = QtCore.Signal(object)  # list of users
    # signal emitted whenever the users changed:
    users_changed = QtCore.Signal(object)  # list of users

    def __init__(self, parent):
        """
        Construction

        :param parent:  The parent QObject
        """
        QtCore.QObject.__init__(self, parent)

        self._show_all_versions = False
        self._filter_reg_exp = QtCore.QRegExp()
        self._reset_user_lists()

    # @property
    def _get_show_all_versions(self):
        return self._show_all_versions

    # @show_all_versions.setter
    def _set_show_all_versions(self, show):
        if show != self._show_all_versions:
            self._show_all_versions = show
            self.changed.emit()

    show_all_versions = property(_get_show_all_versions,
                                 _set_show_all_versions)

    # @property filter_reg_exp
    def _get_filter_reg_exp(self):
        return self._filter_reg_exp

    # @filter_reg_exp.setter
    def _set_filter_reg_exp(self, value):
        if value != self._filter_reg_exp:
            self._filter_reg_exp = value
            self.changed.emit()

    filter_reg_exp = property(_get_filter_reg_exp, _set_filter_reg_exp)

    @property
    def available_users(self):
        """
        :returns: List of available user sandboxes.
        """
        return self._available_users

    def clear_available_users(self):
        """
        Clear the list of available user sandboxes.
        """
        self._reset_user_lists()
        self.available_users_changed.emit(self._available_users)

    def _reset_user_lists(self):
        self._available_users = ([g_user_cache.current_user]
                                 if g_user_cache.current_user else [])
        self._users = [g_user_cache.current_user
                       ] if g_user_cache.current_user else []

    def add_users(self, users):
        """
        Adds to the list of available user sandboxes.

        :param users: List of users dictionaries.
        """
        nb_users_before = len(self._available_users)

        # merge the two lists, discarding doubles.
        new_users_by_id = dict((user["id"], user) for user in users)
        available_users_by_id = dict(
            (user["id"], user) for user in self._available_users)
        available_users_by_id.update(new_users_by_id)
        self._available_users = list(available_users_by_id.values())

        # The updated dictionary has grown, so something new was added!
        if len(self._available_users) > nb_users_before:
            self.available_users_changed.emit(self._available_users)

    # @property
    def _get_users(self):
        return self._users

    # users.setter
    def _set_users(self, users):
        current_user_ids = set([u["id"] for u in self._users if u])
        available_user_ids = set([u["id"] for u in self._available_users])
        new_user_ids = set([u["id"] for u in users if u]) & available_user_ids
        if new_user_ids != current_user_ids:
            self._users = [u for u in users if u and u["id"] in new_user_ids]
            self.users_changed.emit(self._users)
            self.changed.emit()

    users = property(_get_users, _set_users)
Example #10
0
class UserFilterMenu(QtGui.QMenu):
    """
    """
    users_selected = QtCore.Signal(object)# list of users
    
    class _User(object):
        def __init__(self, user, action=None):
            self.user = user
            self.action = action
            self.available = True
    
    def __init__(self, parent):
        """
        """
        QtGui.QMenu.__init__(self, parent)

        self._current_user_id = g_user_cache.current_user["id"] if g_user_cache.current_user else None
        self._available_users = {}
        self._checked_user_ids = set()

        # build the base menu - this won't change:
        menu_action = QtGui.QWidgetAction(self)
        menu_label = QtGui.QLabel("<i>Choose Who To Display Files For</i>", self)
        ss = "QLabel {margin: 3px;}"
        menu_label.setStyleSheet(ss)
        menu_action.setDefaultWidget(menu_label)
        self.addAction(menu_action)
        self.addSeparator()

        self._current_user_action = QtGui.QAction("Show My Files", self)
        self._current_user_action.setCheckable(True)
        toggled_slot = lambda toggled: self._on_user_toggled(self._current_user_id, toggled)
        self._current_user_action.toggled.connect(toggled_slot)
        self.addAction(self._current_user_action)

        self._all_users_action = QtGui.QAction("Show Files For All Other Users", self)
        self._all_users_action.setCheckable(True)
        self._all_users_action.toggled.connect(self._on_all_other_users_toggled)
        self.addAction(self._all_users_action)

        menu_action = QtGui.QWidgetAction(self)
        menu_label = QtGui.QLabel("<i>Other Users:</i>", self)
        ss = "QLabel {margin: 3px;margin-top: 6px;}"
        menu_label.setStyleSheet(ss)
        menu_action.setDefaultWidget(menu_label)
        self.addAction(menu_action)
        self.addSeparator()
        
        self._no_other_users_action = self._add_no_other_users_action()

    @property
    def current_user_selected(self):
        """
        """
        return self._current_user_id in self._checked_user_ids

    @property
    def other_users_selected(self):
        """
        """
        available_user_ids = set([user_id for user_id, details in self._available_users.iteritems() if details.available])
        selected_user_ids = self._checked_user_ids & set(available_user_ids)
        return (len(selected_user_ids) > 1 
                or (len(selected_user_ids) == 1 and next(iter(selected_user_ids)) != self._current_user_id)) 

    #@property
    def _get_selected_users(self):
        available_user_ids = set([user_id for user_id, details in self._available_users.iteritems() if details.available])
        selected_user_ids = self._checked_user_ids & available_user_ids
        users = [self._available_users[id].user for id in selected_user_ids]
        if self._current_user_id in self._checked_user_ids:
            users = [g_user_cache.current_user] + users
        return users
    #selected_users.setter
    def _set_selected_users(self, users):
        self._update_selected_users(users)
    selected_users = property(_get_selected_users, _set_selected_users)

    #@property
    def _get_available_users(self):
        available_users = set([details.user for details in self._available_users.values() if details.available])
        return available_users
    def _set_available_users(self, users):
        self._populate_available_users(users)
    available_users = property(_get_available_users, _set_available_users)

    def _update_selected_users(self, users):
        """
        """
        signals_blocked = self.blockSignals(True)
        try:
            new_checked_user_ids = set()
            user_ids = set([u["id"] for u in users if u])
            for uid in user_ids:
                details = self._available_users.get(uid)
                if not details or not details.available:
                    continue
                details.action.setChecked(True)
                new_checked_user_ids.add(uid)

            if self._current_user_id in user_ids:
                self._current_user_action.setChecked(True)
                new_checked_user_ids.add(self._current_user_id)
            else:
                self._current_user_action.setChecked(False)

            to_uncheck = self._checked_user_ids - new_checked_user_ids
            for uid in to_uncheck:
                details = self._available_users.get(uid)
                if not details or not details.available:
                    continue
                details.action.setChecked(False)

            self._checked_user_ids = new_checked_user_ids

        finally:
            self.blockSignals(signals_blocked)
        
    def _populate_available_users(self, users):
        """
        """
        all_users_checked = self._all_users_action.isChecked()

        # compile a list of users with existing actions if they have them:
        users_changed = False
        available_users = {}
        user_names_and_ids = []
        for user in users:
            if not user:
                continue

            user_name = user["name"]
            user_id = user["id"]

            if user_id == self._current_user_id:
                # don't add current user to list of other users!
                continue

            user_details = self._available_users.get(user_id)
            if user_details is None:
                # new user not currently in the menu:
                user_details = UserFilterMenu._User(user)
            elif not user_details.available:
                # this was a previously unavailable user!
                user_details.available = True
                if user_details.action.isChecked():
                    # enabling a previously disabled checked user so the users will change
                    self._checked_user_ids.add(user_id)
                    users_changed = True

            available_users[user_id] = user_details
            user_names_and_ids.append((user_name, user_id))

        # add any users to the list that are in not in users list but are currently
        # checked in the menu - these will be disabled rather than removed.  Remove
        # all other unchecked users that aren't in the users list:
        user_ids_to_remove = set(self._available_users.keys()) - set(available_users.keys())
        for id in user_ids_to_remove:
            user_details = self._available_users[id]
            if user_details.action.isChecked():
                user_details.available = False
                user_name = user_details.user["name"]
                user_id = user_details.user["id"]
                user_names_and_ids.append((user_name, user_id))
                available_users[id] = user_details
                user_details.action.setEnabled(False)
            else:
                # action is no longer needed so remove:
                self.removeAction(user_details.action)

        # sort list of users being displayed in the menu alphabetically:
        user_names_and_ids.sort(lambda x, y: cmp(x[0].lower(), y[0].lower()) or cmp(x[1], y[1]))

        # add menu items for new users as needed:
        actions_to_insert = []
        for user_name, user_id in user_names_and_ids:
            user_details = available_users.get(user_id)
            if user_details.action:
                # already have an action so insert any actions to insert before it:
                for action in actions_to_insert:
                    self.insertAction(user_details.action, action)
                actions_to_insert = []

                # make sure the action is enabled:
                if user_details.available:
                    user_details.action.setEnabled(True)

                continue

            # need to create a new action:
            action = QtGui.QAction(user_name, self)
            action.setCheckable(True)
            toggled_slot = lambda toggled, uid=user_id: self._on_user_toggled(uid, toggled)
            action.toggled.connect(toggled_slot)

            # Figure out if the user should be checked:
            if user_id in self._checked_user_ids or all_users_checked:
                self._checked_user_ids.add(user_id)
                action.setChecked(True)
                users_changed = True

            # keep track of the new action:
            user_details.action = action
            actions_to_insert.append(action)

        # if there are any actions left to insert then append them at the end of the menu:
        for action in actions_to_insert:
            self.addAction(action)

        # update list of available users (the ones with menu items)
        self._available_users = available_users

        # update checked state of all users action:
        self._update_all_users_action()

        # update 'no other users' action:
        have_other_users = bool(self._available_users)
        if have_other_users and self._no_other_users_action:
            self.removeAction(self._no_other_users_action)
            self._no_other_users_action = None
        elif not have_other_users and not self._no_other_users_action:
            self._no_other_users_action = self._add_no_other_users_action()

        if users_changed:
            # list of selected users has changed so emit changed signal:
            self._emit_users_selected()

    def _add_no_other_users_action(self):
        """
        """
        action = QtGui.QWidgetAction(self)
        menu_label = QtGui.QLabel("<i>(No Other Users Found!)</i>", self)
        ss = "QLabel {margin: 3px;}"
        menu_label.setStyleSheet(ss)
        action.setDefaultWidget(menu_label)
        action.setEnabled(False)
        self.addAction(action)
        return action

    def clear(self):
        """
        """
        # clearing this menu just clears the list of available users:
        for user_details in self._available_users.values():
            self.removeAction(user_details.action)
        self._available_users = {}

    def mousePressEvent(self, event):
        """
        """
        active_action = self.activeAction()
        if active_action and active_action.isCheckable():
            if active_action.isEnabled():
                active_action.toggle()
            return True
        else:
            QtGui.QMenu.mousePressEvent(self, event)

    def _on_user_toggled(self, user_id, toggled):
        """
        """
        users_changed = False
        if toggled:
            if user_id not in self._checked_user_ids:
                self._checked_user_ids.add(user_id)
                users_changed = True
        else:
            if user_id in self._checked_user_ids:
                self._checked_user_ids.remove(user_id)
                users_changed = True

        # make sure that the 'all users' checkbox is up-to-date:
        self._update_all_users_action()

        if users_changed:
            self._emit_users_selected()

    def _update_all_users_action(self):
        """
        """
        all_users_checked = True
        all_users_enabled = False
        if not self._available_users:
            # there are no available users so checkbox should be disabled and
            # unchecked:
            all_users_enabled = False
            all_users_checked = False
        else:
            # figure out if we have any un-checked available users:
            for user_details in self._available_users.values():
                if not user_details.action.isChecked():
                    all_users_checked = False
                if user_details.available:
                    all_users_enabled = True
                if all_users_enabled and not all_users_checked:
                    break

        # update action:
        self._all_users_action.setEnabled(all_users_enabled)
        if self._all_users_action.isChecked() != all_users_checked:
            signals_blocked = self._all_users_action.blockSignals(True)
            try:
                self._all_users_action.setChecked(all_users_checked)
            finally:
                self._all_users_action.blockSignals(signals_blocked)

    def _on_all_other_users_toggled(self, toggled):
        """
        """
        signals_blocked = self.blockSignals(True)
        #users_changed = False
        try:
            # toggle all other user actions:
            for user_details in self._available_users.values():
                if user_details.action.isChecked() != toggled:
                    #users_changed= True
                    user_details.action.setChecked(toggled)
        finally:
            self.blockSignals(signals_blocked)

        #if users_changed:
        self._emit_users_selected()

    def _emit_users_selected(self):
        """
        """
        self.users_selected.emit(self.selected_users)
 class Notifier(QtCore.QObject):
     different_user_requested = QtCore.Signal(str, int)
Example #12
0
class RecentButton(QtGui.QPushButton):
    """
    The RecentButton has an icon and text underneath it and can launch
    a single action, unlike the CommandButton.
    """

    MARGIN = 5
    SPACING = 5
    SIZER_LABEL = None

    command_triggered = QtCore.Signal(str)

    def __init__(self, parent, command_name, button_name, icon, tooltip,
                 timestamp):
        """
        :param str command_name: Name of the command.
        :param str button_name: Name of the button.
        :param str icon: Path to the icon for this command.
        :param str tooltip: Toolkit for this command.
        :param datetime.datetime timestamp: When the command was last launched.
        """
        super(RecentButton, self).__init__(parent)

        # No borders
        self.setFlat(True)

        self.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding,
                           QtGui.QSizePolicy.MinimumExpanding)
        self.setFocusPolicy(QtCore.Qt.NoFocus)

        layout = QtGui.QVBoxLayout(self)
        layout.setAlignment(QtCore.Qt.AlignHCenter)
        layout.setSpacing(self.SPACING)
        layout.setContentsMargins(self.MARGIN, self.MARGIN, self.MARGIN,
                                  self.MARGIN)

        self._timestamp = timestamp

        self.icon_label = QtGui.QLabel(self)
        self.icon_label.setAlignment(QtCore.Qt.AlignHCenter)
        self.layout().addWidget(self.icon_label, QtCore.Qt.AlignHCenter)

        self.text_label = QtGui.QLabel(parent)
        self.text_label.setWordWrap(True)
        self.text_label.setAlignment(QtCore.Qt.AlignHCenter)
        self.layout().addWidget(self.text_label, QtCore.Qt.AlignHCenter)

        self.setFocusPolicy(QtCore.Qt.NoFocus)
        self.setStyleSheet(BUTTON_STYLE)

        self.text_label.setText(button_name)
        if icon is None:
            self.icon_label.setPixmap(QtGui.QIcon().pixmap(ICON_SIZE))
        else:
            self.icon_label.setPixmap(QtGui.QIcon(icon).pixmap(ICON_SIZE))

        self.setToolTip(tooltip)

        self._command_name = command_name

        self.clicked.connect(lambda: self.command_triggered.emit(
            six.ensure_str(self._command_name)))

    @property
    def name(self):
        """
        Name of the button.
        """
        return six.ensure_str(self.text_label.text())

    @property
    def timestamp(self):
        """
        Time when this command was last executed.
        """
        return self._timestamp

    @property
    def command_name(self):
        """
        Name of the command.
        """
        return self._command_name

    def sizeHint(self):
        """
        Hint at the button size.

        The button should occupy 1/MAX_RECENTS's of the parent widget width
        and be a bit higher than the icon.
        """
        hint = QtCore.QSize(
            (self.parentWidget().width() / MAX_RECENTS) - (self.SPACING * 2),
            ICON_SIZE.height() + 8,
        )
        return hint
Example #13
0
class VersionDetailsWidget(QtGui.QWidget):
    """
    QT Widget that displays details and Note thread data for a given Version
    entity.

    :signal entity_created(object): Fires when a Note or Reply entity is created by
            an underlying widget within the activity stream. Passes on a Shotgun
            entity definition in the form of a dict.
    :signal entity_loaded(object): Fires when a Version entity has been loaded by
            the widget. Passes on a Shotgun entity definition in the form of a dict.
    :signal note_selected(int): Fires when a Note entity is selected in the widget's
            note thread stream. Passes on the entity id of the selected note.
    :signal note_deselected(int): Fires when a Note entity is deselected. Passes on
            the entity id of the selected note.
    :signal note_arrived(int, object): Fires when a new Note entity arrives and is
            displayed in the widget's note thread stream. Passes on the entity id
            and Shotgun entity definition as an int and dict, respectively.
    :signal note_metadata_changed(int, str): Fires when the widget successfully
            updates a Note entity's metadata field. The Note entity's id and the
            new metadata are passed on.
    :signal note_attachment_arrived(int, str): Fires when an attachment file
            associated with a Note entity is successfully downloaded. The Note
            entity id and the path to the file on disk are passed on.
    """
    FIELDS_PREFS_KEY = "version_details_fields"
    ACTIVE_FIELDS_PREFS_KEY = "version_details_active_fields"
    VERSION_LIST_FIELDS_PREFS_KEY = "version_details_version_list_fields"
    NOTE_METADATA_FIELD = "sg_metadata"
    NOTE_MARKUP_PREFIX = "__note_markup__"

    # Emitted when an entity is created by the panel. The
    # entity type as a string and id as an int are passed
    # along.
    entity_created = QtCore.Signal(object)

    # Emitted when an entity is loaded in the panel.
    entity_loaded = QtCore.Signal(object)

    # The int is the id of the Note entity that was selected or deselected.
    note_selected = QtCore.Signal(int)
    note_deselected = QtCore.Signal(int)
    note_arrived = QtCore.Signal(int, object)
    note_metadata_changed = QtCore.Signal(int, str)
    note_attachment_arrived = QtCore.Signal(int, str)

    def __init__(self, bg_task_manager, parent=None, entity=None):
        """
        Constructs a new :class:`~VersionDetailsWidget` object.

        :param parent:          The widget's parent.
        :param bg_task_manager: A :class:`~BackgroundTaskManager` object.
        :param entity:          A Shotgun Version entity dictionary.
        """
        super(VersionDetailsWidget, self).__init__(parent)

        self._current_entity = None
        self._pinned = False
        self._requested_entity = None
        self._sort_versions_ascending = False
        self._upload_task_ids = []
        self._task_manager = bg_task_manager
        self._version_context_menu_actions = []
        self._note_metadata_uids = []
        self._note_set_metadata_uids = []
        self._uploads_uids = []
        self._attachment_uids = {}
        self._note_fields = [self.NOTE_METADATA_FIELD]
        self._attachments_filter = None

        self.ui = Ui_VersionDetailsWidget()
        self.ui.setupUi(self)

        # Show the "empty" image that tells the user that no Version
        # is active.
        self.ui.pages.setCurrentWidget(self.ui.empty_page)

        self._data_retriever = shotgun_data.ShotgunDataRetriever(
            parent=self,
            bg_task_manager=self._task_manager,
        )

        self._shotgun_field_manager = ShotgunFieldManager(
            self,
            bg_task_manager=self._task_manager,
        )

        self._settings_manager = settings.UserSettings(
            sgtk.platform.current_bundle(), )

        shotgun_globals.register_bg_task_manager(self._task_manager)
        self._shotgun_field_manager.initialize()

        # These are the minimum required fields that we need
        # in order to draw all of our widgets with default settings.
        self._fields = [
            "image",
            "user",
            "project",
            "code",
            "id",
            "entity",
            "sg_status_list",
        ]

        prefs_fields = self._settings_manager.retrieve(
            VersionDetailsWidget.FIELDS_PREFS_KEY,
            [],
            self._settings_manager.SCOPE_ENGINE,
        )

        self._fields.extend([f for f in prefs_fields if f not in self._fields])

        # These are the fields that have been given to the info widget
        # at the top of the Notes tab. This represents all fields that
        # are displayed by default when the "More info" option is active.
        self._active_fields = self._settings_manager.retrieve(
            VersionDetailsWidget.ACTIVE_FIELDS_PREFS_KEY,
            [
                "code",
                "entity",
                "user",
                "sg_status_list",
            ],
            self._settings_manager.SCOPE_ENGINE,
        )

        # This is the subset of self._active_fields that are always
        # visible, even when "More info" is not active.
        self._persistent_fields = [
            "code",
            "entity",
        ]

        # The fields list for the Version list view delegate operate
        # the same way as the above persistent list. We're simply
        # keeping track of what we don't allow to be turned off.
        self._version_list_persistent_fields = [
            "code",
            "user",
            "sg_status_list",
        ]

        # Our sort-by list will include "id" at the head.
        self._version_list_sort_by_fields = [
            "id"
        ] + self._version_list_persistent_fields

        self.version_model = SimpleTooltipModel(
            self.ui.entity_version_tab,
            bg_task_manager=self._task_manager,
        )
        self.version_proxy_model = ShotgunSortFilterProxyModel(
            parent=self.ui.entity_version_view, )
        self.version_proxy_model.filter_by_fields = self._version_list_persistent_fields
        self.version_proxy_model.sort_by_fields = self._version_list_sort_by_fields
        self.version_proxy_model.setFilterWildcard("*")
        self.version_proxy_model.setSourceModel(self.version_model)
        self.ui.entity_version_view.setModel(self.version_proxy_model)
        self.version_delegate = ShotgunEntityCardDelegate(
            view=self.ui.entity_version_view,
            shotgun_field_manager=self._shotgun_field_manager,
            parent=self,
        )
        self.version_delegate.fields = self._settings_manager.retrieve(
            VersionDetailsWidget.VERSION_LIST_FIELDS_PREFS_KEY,
            self._version_list_persistent_fields,
            self._settings_manager.SCOPE_ENGINE,
        )
        self.version_delegate.label_exempt_fields = ["code"]
        self.ui.entity_version_view.setItemDelegate(self.version_delegate)
        self.ui.note_stream_widget.set_bg_task_manager(self._task_manager)
        self.ui.note_stream_widget.show_sg_stream_button = False
        self.ui.note_stream_widget.version_items_playable = False
        self.ui.note_stream_widget.clickable_user_icons = False
        self.ui.note_stream_widget.show_note_links = False
        self.ui.note_stream_widget.highlight_new_arrivals = False
        self.ui.note_stream_widget.notes_are_selectable = True
        self.version_info_model = shotgun_model.SimpleShotgunModel(
            self.ui.note_stream_widget,
            bg_task_manager=self._task_manager,
        )

        # For the basic info widget in the Notes stream we won't show
        # labels for the fields that are persistent. The non-standard,
        # user-specified list of fields that are shown when "more info"
        # is active will be labeled.
        self.ui.current_version_card.field_manager = self._shotgun_field_manager
        self.ui.current_version_card.fields = self._active_fields
        self.ui.current_version_card.label_exempt_fields = self._persistent_fields

        # Signal handling.
        self.ui.pin_button.toggled.connect(self.set_pinned)
        self.ui.more_info_button.toggled.connect(self._more_info_toggled)
        self.ui.shotgun_nav_button.clicked.connect(
            self.ui.note_stream_widget._load_shotgun_activity_stream)
        self.ui.entity_version_view.customContextMenuRequested.connect(
            self._show_version_context_menu, )
        self.ui.version_search.search_edited.connect(
            self._set_version_list_filter)
        self.version_info_model.data_refreshed.connect(
            self._version_entity_data_refreshed)
        self._task_manager.task_completed.connect(self._on_task_completed)
        self.ui.note_stream_widget.note_selected.connect(
            self.note_selected.emit)
        self.ui.note_stream_widget.note_deselected.connect(
            self.note_deselected.emit)
        self.ui.note_stream_widget.note_arrived.connect(
            self._process_note_arrival)

        self._data_retriever.work_completed.connect(self.__on_worker_signal)
        self._data_retriever.work_failure.connect(self.__on_worker_failure)

        # We're taking over the responsibility of handling the title bar's
        # typical responsibilities of closing the dock and managing float
        # and unfloat behavior. We need to hook up to the dockLocationChanged
        # signal because a floating DockWidget can be redocked with a
        # double click of the window, which won't go through our button.
        self.ui.float_button.clicked.connect(self._toggle_floating)
        self.ui.close_button.clicked.connect(self._hide_dock)

        # We will be passing up our own signal when note and reply entities
        # are created.
        self.ui.note_stream_widget.entity_created.connect(
            self._entity_created, )

        self.load_data(entity)
        self._load_stylesheet()
        self.show_title_bar_buttons(False)

        # This will handle showing or hiding the dock title bar
        # depending on what the parent is.
        self.setParent(parent)

    ##########################################################################
    # properties

    @property
    def current_entity(self):
        """
        The current Shotgun entity that is active in the widget.
        """
        return self._current_entity

    @property
    def is_pinned(self):
        """
        Returns True if the panel is pinned and not processing entity
        updates, and False if it is not pinned.
        """
        return self._pinned

    def _get_note_fields(self):
        """
        The list of Note entity field names that are queried and provided
        when note_arrived is emitted.

        :returns:   list(str, ...)
        """
        return self._note_fields

    def _set_note_fields(self, fields):
        self._note_fields = list(fields)

    note_fields = property(_get_note_fields, _set_note_fields)

    @property
    def note_threads(self):
        """
        The currently loaded Note threads keyed by Note entity id and
        containing a list of Shotgun entity dictionaries.

        Example structure containing a single Note entity:
            6038: [
                {
                    'content': 'This is a test note.',
                    'created_by': {
                        'id': 39,
                        'name': 'Jeff Beeland',
                        'type': 'HumanUser'
                    },
                    'id': 6038,
                    'sg_metadata': None,
                    'type': 'Note'
                }
            ]
        """
        return self.ui.note_stream_widget.note_threads

    def _get_attachments_filter(self):
        """
        If set to a compiled regular expression, attachment file names that match
        will be filtered OUT and NOT shown.
        """
        return self._attachments_filter

    def _set_attachments_filter(self, regex):
        self._attachments_filter = regex
        self.ui.note_stream_widget.attachments_filter = regex

    attachments_filter = property(_get_attachments_filter,
                                  _set_attachments_filter)

    ##########################################################################
    # public methods

    def add_note_attachments(self,
                             file_paths,
                             note_entity,
                             cleanup_after_upload=True):
        """
        Adds a given list of files to the note widget as file attachments.

        :param file_paths:              A list of file paths to attach to the
                                        current note.
        :param cleanup_after_upload:    If True, after the files are uploaded
                                        to Shotgun they will be removed from disk.
        """
        if note_entity["type"] == "Reply":
            note_entity = note_entity["entity"]

        for file_path in file_paths:
            self._upload_uids.append(
                self._data_retriever.execute_method(
                    self.__upload_file,
                    dict(
                        file_path=file_path,
                        parent_entity_type=note_entity["type"],
                        parent_entity_id=note_entity["id"],
                        cleanup_after_upload=cleanup_after_upload,
                    ),
                ))

    def add_query_fields(self, fields):
        """
        Adds the given list of Shotgun field names to the list of fields
        that are queried by the version details widget's internal data
        model. Adding fields this way does not change the display of
        information about the entity in any way.

        :param fields:  A list of Shotgun field names to add.
        :type fields:   [field_name, ...]
        """
        self._fields.extend([f for f in fields if f not in self._fields])

    def add_version_context_menu_action(self, action_definition):
        """
        Adds an action to the version tab's context menu.

        Action definitions passed in must take the following form:

            dict(
                callback=callable,
                text=str,
                required_selection="single"
            )

        Where the callback is a callable object that expects to receive
        a list of Version entity dictionaries as returned by the Shotgun
        Python API. The text key contains the string labels of the action
        in the QMenu, and the required_selection is one of "single", "multi",
        or "either". Any action requiring a "single" selection will be enabled
        only if there is a single item selected in the Version list view,
        those requiring "multi" selection require 2 or more selected items,
        and the "either" requirement results in the action being enabled if
        one or more items are selected.

        :param action_definition:   The action defition to add to the menu.
                                    This takes the form of a dictionary of
                                    a structure described in the method docs
                                    above.
        :type action_definition:    dict
        """
        self._version_context_menu_actions.append(action_definition)

    def clear(self):
        """
        Clears all data from all widgets and views in the details panel.
        """
        self._more_info_toggled(False)
        self.ui.note_stream_widget._clear()
        self.ui.current_version_card.clear()
        self.ui.pages.setCurrentWidget(self.ui.empty_page)
        self.version_model.clear()
        self.version_info_model.clear()
        self._requested_entity = None
        self._current_entity = None

    def deselect_note(self):
        """
        If a note is currently selected, it will be deselected. This will NOT
        trigger a note_deselected signal to be emitted, as that is only emitted
        when the user triggers the deselection and not via procedural means.
        """
        self.ui.note_stream_widget.deselect_note()

    def download_note_attachments(self, note_id):
        """
        Triggers the attachments linked to the given Note entity to
        be downloaded. When the download is completed successfully, an
        attachment_downloaded signal is emitted.

        :param int note_id: The Note entity id.
        """
        attachments = self.get_note_attachments(note_id)

        for attachment in attachments:
            uid = self._data_retriever.request_attachment(attachment)
            self._attachment_uids[uid] = note_id

    def get_note_attachments(self, note_id):
        """
        Gets the Attachment entities associated with the given Note
        entity.

        :param int note_id: The Note entity id.
        """
        return self.ui.note_stream_widget.get_note_attachments(note_id)

    def load_data(self, entity):
        """
        Loads the given Shotgun entity into the details panel,
        triggering the notes and versions streams to be updated
        relative to the given entity.

        :param entity:  The Shotgun entity to load. This is a dict in
                        the form returned by the Shotgun Python API.
        """
        self._requested_entity = entity

        # If we're pinned, then we don't allow loading new entities.
        if self._pinned and self.current_entity:
            return

        # If we got an "empty" entity from the mode, then we need
        # to clear everything out and go back to an empty state.
        if not entity or not entity.get("id"):
            self.clear()
            return

        # Switch over to the page that contains the primary display
        # widget set now that we have data to show.
        self.ui.pages.setCurrentWidget(self.ui.main_page)

        # If there aren't any fields set in the info widget then it
        # likely means we're loading from a "cleared" slate and need
        # to re-add our relevant fields.
        if not self.ui.current_version_card.fields:
            self.ui.current_version_card.fields = self._active_fields
            self.ui.current_version_card.label_exempt_fields = self._persistent_fields

        self.ui.note_stream_widget.load_data(entity)

        shot_filters = [["id", "is", entity["id"]]]
        self.version_info_model.load_data(
            entity_type="Version",
            filters=shot_filters,
            fields=self._fields,
        )

        for note_id in self.note_threads.keys():
            self._process_note_arrival(note_id)

        self.entity_loaded.emit(entity)

    def save_preferences(self):
        """
        Saves user preferences to disk.
        """
        self._settings_manager.store(
            VersionDetailsWidget.FIELDS_PREFS_KEY,
            self._fields,
            self._settings_manager.SCOPE_ENGINE,
        )

        self._settings_manager.store(
            VersionDetailsWidget.ACTIVE_FIELDS_PREFS_KEY,
            self._active_fields,
            self._settings_manager.SCOPE_ENGINE,
        )

        self._settings_manager.store(
            VersionDetailsWidget.VERSION_LIST_FIELDS_PREFS_KEY,
            self.version_delegate.fields,
            self._settings_manager.SCOPE_ENGINE,
        )

    def set_note_metadata(self, note_id, metadata):
        """
        Sets a Note entity's metadata in Shotgun.

        :param int note_id: The Note entity id.
        :param str metadata: The metadata to set in Shotgun.
        """
        self._note_set_metadata_uids.append(
            self._data_retriever.execute_update(
                "Note",
                note_id,
                {self.NOTE_METADATA_FIELD: metadata},
            ))

    def set_note_screenshot(self, image_path):
        """
        Takes the given file path to an image and sets the new note
        widget's thumbnail image.

        :param str image_path:  A file path to an image file on disk.
        """
        self.ui.note_stream_widget.note_widget._set_screenshot_pixmap(
            QtGui.QPixmap(image_path), )

    def set_pinned(self, checked):
        """
        Sets the "pinned" state of the details panel. When the panel is
        pinned it will not accept updates. It will, however, record the
        most recent entity passed to load_data that was not accepted. If
        the panel is unpinned at a later time, the most recent rejected
        entity update will be executed at that time.

        :param checked: True or False
        """
        self._pinned = checked

        if checked:
            self.ui.pin_button.setIcon(
                QtGui.QIcon(":/version_details/tack_hover.png"))
        else:
            self.ui.pin_button.setIcon(
                QtGui.QIcon(":/version_details/tack_up.png"))
            if self._requested_entity:
                self.load_data(self._requested_entity)

    def show_new_note_dialog(self, modal=True):
        """
        Shows a dialog that allows the user to input a new note.

        :param modal:   Whether the dialog should be shown modally or not.
        :type modal:    bool
        """
        self.ui.note_stream_widget.show_new_note_dialog(modal=modal)

    def show_title_bar_buttons(self, state):
        """
        Sets the visibility of the undock and close buttons in the
        widget's title bar.

        :param state:   Whether to show or hide the buttons.
        :type state:    bool
        """
        self.ui.float_button.setVisible(state)
        self.ui.close_button.setVisible(state)

    def set_version_thumbnail(self, thumbnail_path, version_id=None):
        """
        Sets a Version entity's thumbnail image in Shotgun. If no Version
        id is provided, the current Version entity will be updated.

        :param str thumbnail_path: The path to the thumbnail file on disk.
        :param int version_id: The Version entity's id. If not provided
                               then the current Version entity loaded in
                               the widget will be used.
        """
        if not version_id and not self.current_entity:
            return

        version_id = version_id or self.current_entity["id"]
        self._data_retriever.execute_method(
            self.__upload_thumbnail,
            dict(
                entity_type="Version",
                entity_id=version_id,
                path=thumbnail_path,
            ),
        )

    ##########################################################################
    # internal utilities

    def _process_note_arrival(self, note_id):
        """
        When a new Note entity arrives from Shotgun in the version details
        widget, Dynamite is notified and provided the Note entity's metadata.

        :param note_id:     The id of the Note entity.
        :type note_id:      int
        """
        entity_fields = dict(
            Note=[self.NOTE_METADATA_FIELD],
            Reply=[self.NOTE_METADATA_FIELD],
        )

        self._note_metadata_uids.append(
            self._data_retriever.execute_find_one(
                "Note",
                [["id", "is", note_id]],
                self.note_fields,
            ))

    def _on_task_completed(self):
        """
        Signaled whenever the worker completes something. This method will
        dispatch the work to different methods depending on what async task
        has completed.
        """
        self.ui.entity_version_view.repaint()
        self.ui.current_version_card.repaint()
        self.ui.note_stream_widget.repaint()

    def __on_worker_signal(self, uid, request_type, data):
        """
        Signaled whenever the worker completes something. This method will
        dispatch the work to different methods depending on what async task
        has completed.

        :param uid:             Unique id for the request.
        :type uid:              int
        :param request_type:    The request class.
        :type request_type:     str
        :param data:            The returned data.
        :type data:             dict 
        """
        if uid in self._note_metadata_uids:
            entity = data["sg"]
            self.note_arrived.emit(entity["id"], entity)
        elif uid in self._note_set_metadata_uids:
            entity = data["sg"]
            self.note_metadata_changed.emit(
                entity["id"],
                entity[self.NOTE_METADATA_FIELD],
            )
        elif uid in self._attachment_uids:
            note_id = self._attachment_uids[uid]
            del self._attachment_uids[uid]
            self.note_attachment_arrived.emit(note_id, data["file_path"])
        elif uid in self._upload_uids:
            self.ui.note_stream_widget.rescan(
                force_activity_stream_update=True)

    def __on_worker_failure(self, uid, msg):
        """
        Asynchronous callback - the worker thread errored.
        
        :param uid: Unique id for request that failed.
        :type uid:  int
        :param msg: The error message.
        :type msg:  str
        """
        if uid in self._note_metadata_uids:
            sgtk.platform.current_bundle().log_error(msg)
        elif uid in self._attachment_uids:
            sgtk.platform.current_bundle().log_error(msg)

    def _load_stylesheet(self):
        """
        Loads in the widget's master stylesheet from disk.
        """
        qss_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                "style.qss")
        try:
            f = open(qss_file, "rt")
            qss_data = sgtk.platform.current_bundle(
            ).engine._resolve_sg_stylesheet_tokens(f.read(), )
            self.setStyleSheet(qss_data)
        finally:
            f.close()

    def _entity_created(self, entity):
        """
        Emits the entity_created signal.

        :param entity: The Shotgun entity dict that was created.
        """
        self.entity_created.emit(entity)

    def _field_menu_triggered(self, action):
        """
        Adds or removes a field when it checked or unchecked
        via the EntityFieldMenu.

        :param action:  The QMenuAction that was triggered. 
        """
        if action:
            # The MenuAction's data will have a "field" key that was
            # added by the EntityFieldMenu that contains the name of
            # the field that was checked or unchecked.
            field_name = action.data()["field"]

            if action.isChecked():
                self.ui.current_version_card.add_field(field_name)
                self._fields.append(field_name)
                self.load_data(self._requested_entity)
            else:
                self.ui.current_version_card.remove_field(field_name)

            self._active_fields = self.ui.current_version_card.fields

            try:
                self._settings_manager.store(
                    VersionDetailsWidget.FIELDS_PREFS_KEY,
                    self._fields,
                    self._settings_manager.SCOPE_ENGINE,
                )
                self._settings_manager.store(
                    VersionDetailsWidget.ACTIVE_FIELDS_PREFS_KEY,
                    self._active_fields,
                    self._settings_manager.SCOPE_ENGINE,
                )
            except Exception:
                pass

    def _version_entity_data_refreshed(self):
        """
        Takes the currently-requested entity and sets various widgets
        to display it.
        """
        entity = self._requested_entity

        if not entity:
            return

        item = self.version_info_model.item_from_entity(
            "Version",
            entity["id"],
        )

        if not item:
            return

        sg_data = item.get_sg_data()

        self.ui.current_version_card.entity = sg_data
        self._more_info_toggled(self.ui.more_info_button.isChecked())

        if sg_data.get("entity"):
            version_filters = [["entity", "is", sg_data["entity"]]]
            self.version_model.load_data(
                "Version",
                filters=version_filters,
                fields=self._fields,
            )

            self.version_proxy_model.sort(
                0,
                (QtCore.Qt.AscendingOrder if self._sort_versions_ascending else
                 QtCore.Qt.DescendingOrder),
            )
        else:
            self.version_model.clear()

        self._current_entity = sg_data
        self._setup_fields_menu()
        self._setup_version_list_fields_menu()
        self._setup_version_sort_by_menu()

    def _version_list_field_menu_triggered(self, action):
        """
        Adds or removes a field when it checked or unchecked
        via the EntityFieldMenu.

        :param action:  The QMenuAction that was triggered. 
        """
        if action:
            # The MenuAction's data will have a "field" key that was
            # added by the EntityFieldMenu that contains the name of
            # the field that was checked or unchecked.
            field_name = action.data()["field"]

            if action.isChecked():
                self.version_delegate.add_field(field_name)

                # When a field is added to the list, then we also need to
                # add it to the sort-by menu.
                if field_name not in self._version_list_sort_by_fields:
                    self._version_list_sort_by_fields.append(field_name)
                    self._fields.append(field_name)
                    self.load_data(self._requested_entity)

                    new_action = QtGui.QAction(
                        shotgun_globals.get_field_display_name(
                            "Version",
                            field_name,
                        ),
                        self,
                    )

                    new_action.setData(field_name)
                    new_action.setCheckable(True)

                    self._version_sort_menu_fields.addAction(new_action)
                    self._version_sort_menu.addAction(new_action)
                    self._sort_version_list()
            else:
                self.version_delegate.remove_field(field_name)

                # We also need to remove the field from the sort-by menu. We
                # will leave "id" in the list always, even if it isn't being
                # displayed by the delegate.
                if field_name != "id" and field_name in self._version_list_sort_by_fields:
                    self._version_list_sort_by_fields.remove(field_name)
                    sort_actions = self._version_sort_menu.actions()
                    remove_action = [
                        a for a in sort_actions if a.data() == field_name
                    ][0]

                    # If it's the current primary sort field, then we need to
                    # fall back on "id" to take its place.
                    if remove_action.isChecked():
                        actions = self._version_sort_menu_fields.actions()
                        id_action = [a for a in actions if a.data() == "id"][0]
                        id_action.setChecked(True)
                        self._sort_version_list(id_action)
                    self._version_sort_menu.removeAction(remove_action)
                    self._version_sort_menu_fields.removeAction(remove_action)

            self.version_proxy_model.filter_by_fields = self.version_delegate.fields
            self.version_proxy_model.setFilterWildcard(
                self.ui.version_search.search_text)
            self.ui.entity_version_view.repaint()

            try:
                self._settings_manager.store(
                    VersionDetailsWidget.FIELDS_PREFS_KEY,
                    self._fields,
                    self._settings_manager.SCOPE_ENGINE,
                )
                self._settings_manager.store(
                    VersionDetailsWidget.VERSION_LIST_FIELDS_PREFS_KEY,
                    self.version_delegate.fields,
                    self._settings_manager.SCOPE_ENGINE,
                )
            except Exception:
                pass

    def _more_info_toggled(self, checked):
        """
        Toggled more/less info functionality for the info widget in the
        Notes tab.

        :param checked: True or False
        """
        if checked:
            self.ui.more_info_button.setText("Hide info")
            self.ui.more_fields_button.show()

            for field_name in self._active_fields:
                self.ui.current_version_card.set_field_visibility(
                    field_name, True)
        else:
            self.ui.more_info_button.setText("More info")
            self.ui.more_fields_button.hide()

            for field_name in self._active_fields:
                if field_name not in self._persistent_fields:
                    self.ui.current_version_card.set_field_visibility(
                        field_name, False)

    def _selected_version_entities(self):
        """
        Returns a list of Version entities that are currently selected.
        """
        selection_model = self.ui.entity_version_view.selectionModel()
        indexes = selection_model.selectedIndexes()
        entities = []

        for i in indexes:
            entity = shotgun_model.get_sg_data(i)
            try:
                image_file = self.version_delegate.widget_cache[
                    i].thumbnail.image_file_path
            except Exception:
                image_file = ""
            entity["__image_path"] = image_file
            entities.append(entity)

        return entities

    def _set_version_list_filter(self, filter_text):
        """
        Sets the Version list proxy model's filter pattern and forces
        a reselection of any items in the list.

        :param filter_text: The pattern to set as the proxy model's
                            filter wildcard.
        """
        # Forcing a reselection handles forcing a rebuild of any
        # editor widgets and will ensure we draw/sort/filter properly.
        self.version_proxy_model.setFilterWildcard(filter_text)
        self.version_delegate.force_reselection()

    def _setup_fields_menu(self):
        """
        Sets up the EntityFieldMenu and attaches it as the "More fields"
        button's menu.
        """
        entity = self.current_entity or {}
        menu = EntityFieldMenu(
            "Version",
            project_id=entity.get("project", {}).get("id"),
            parent=self,
        )
        menu.set_field_filter(self._field_filter)
        menu.set_checked_filter(self._checked_filter)
        menu.set_disabled_filter(self._disabled_filter)
        self._field_menu = menu
        self._field_menu.triggered.connect(self._field_menu_triggered)
        self.ui.more_fields_button.setMenu(menu)

    def _setup_version_list_fields_menu(self):
        """
        Sets up the EntityFieldMenu and attaches it as the "More fields"
        button's menu.
        """
        entity = self.current_entity or {}
        menu = EntityFieldMenu(
            "Version",
            project_id=entity.get("project", {}).get("id"),
            parent=self,
        )
        menu.set_field_filter(self._field_filter)
        menu.set_checked_filter(self._version_list_checked_filter)
        menu.set_disabled_filter(self._version_list_disabled_filter)
        self._version_list_field_menu = menu
        self._version_list_field_menu.triggered.connect(
            self._version_list_field_menu_triggered)
        self.ui.version_fields_button.setMenu(menu)

    def _setup_version_sort_by_menu(self):
        """
        Sets up the sort-by menu in the Versions tab.
        """
        self._version_sort_menu = ShotgunMenu(self)
        self._version_sort_menu.setObjectName("version_sort_menu")

        ascending = QtGui.QAction("Ascending", self)
        descending = QtGui.QAction("Descending", self)
        ascending.setCheckable(True)
        descending.setCheckable(True)
        descending.setChecked(True)

        self._version_sort_menu_directions = QtGui.QActionGroup(self)
        self._version_sort_menu_fields = QtGui.QActionGroup(self)
        self._version_sort_menu_directions.setExclusive(True)
        self._version_sort_menu_fields.setExclusive(True)

        self._version_sort_menu_directions.addAction(ascending)
        self._version_sort_menu_directions.addAction(descending)
        self._version_sort_menu.add_group([ascending, descending],
                                          title="Direction")

        field_actions = []

        for field_name in self._version_list_sort_by_fields:
            display_name = shotgun_globals.get_field_display_name(
                "Version",
                field_name,
            )

            action = QtGui.QAction(display_name, self)

            # We store the database field name on the action, but
            # display the "pretty" name for users.
            action.setData(field_name)
            action.setCheckable(True)
            action.setChecked((field_name == "id"))
            self._version_sort_menu_fields.addAction(action)
            field_actions.append(action)

        self._version_sort_menu.add_group(field_actions, title="By Field")
        self._version_sort_menu_directions.triggered.connect(
            self._toggle_sort_order)
        self._version_sort_menu_fields.triggered.connect(
            self._sort_version_list)
        self.ui.version_sort_button.setMenu(self._version_sort_menu)

    def _show_version_context_menu(self, point):
        """
        Shows the version list context menu containing all available
        actions. Which actions are enabled is determined by how many
        items in the list are selected.

        :param point:   The QPoint location to show the context menu at.
        """
        selection_model = self.ui.entity_version_view.selectionModel()
        versions = self._selected_version_entities()
        menu = SelectionContextMenu(versions)

        for menu_action in self._version_context_menu_actions:
            menu.addAction(action_definition=menu_action)

        # Show the menu at the mouse cursor. Whatever action is
        # chosen from the menu will have its callback executed.
        action = menu.exec_(self.ui.entity_version_view.mapToGlobal(point))
        menu.execute_callback(action)

    def __upload_file(self, sg, data):
        """
        Uploads any generic file attachments to Shotgun, parenting
        them to the given entity.

        :param sg: A Shotgun API instance.
        :param dict data: A dictionary containing "parent_entity_type",
                          "parent_entity_id", "file_path", and
                          "cleanup_after_upload" keys.
        """
        sg.upload(
            data["parent_entity_type"],
            data["parent_entity_id"],
            str(data["file_path"]),
        )

        if data.get("cleanup_after_upload", False):
            try:
                os.remove(data["file_path"])
            except Exception:
                pass

        self.ui.note_stream_widget.rescan(force_activity_stream_update=True)

    def __upload_thumbnail(self, sg, data):
        """
        Uploads an image file as a thumbnail for the given entity. This
        is intended to be used with the execute_method call from a Shotgun
        data retriever object.

        The data dictionary will take the following form:
            dict(
                entity_type=str,
                entity_id=int,
                path=str,
            )

        :param sg: A Shotgun API instance.
        :param dict data: A dictionary of data passed through from the
                          Shotgun data retriever.
        """
        sg.upload_thumbnail(
            data["entity_type"],
            data["entity_id"],
            data["path"],
        )

        self.ui.note_stream_widget.rescan(force_activity_stream_update=True)
        self.ui.current_version_card.thumbnail.setPixmap(
            QtGui.QPixmap(data["path"]))

    ##########################################################################
    # docking

    def _dock_location_changed(self):
        """
        Handles the dock being redocked in some location. This will
        trigger removing the default title bar.
        """
        self.parent().setTitleBarWidget(QtGui.QWidget(parent=self))

    def _hide_dock(self):
        """
        Hides the parent dock widget.
        """
        self.parent().hide()

    def _toggle_floating(self):
        """
        Toggles the parent dock widget's floating status.
        """
        if self.parent().isFloating():
            self.parent().setFloating(False)
            self._dock_location_changed()
        else:
            self.parent().setTitleBarWidget(None)
            self.parent().setFloating(True)

    ##########################################################################
    # version list actions

    def _toggle_sort_order(self):
        """
        Toggles ascending/descending sort ordering in the version list view.
        """
        self._sort_versions_ascending = not self._sort_versions_ascending
        self.version_proxy_model.sort(
            0,
            (QtCore.Qt.AscendingOrder
             if self._sort_versions_ascending else QtCore.Qt.DescendingOrder),
        )

        # We need to force a reselection after sorting. This will
        # remove edit widgets and allow a full repaint of the view,
        # and then reselect to go back to editing.
        self.version_delegate.force_reselection()

    def _sort_version_list(self, action=None):
        """
        Sorts the version list by the field chosen in the sort-by
        menu. This also triggers a reselection in the view in
        order to ensure proper sorting and drawing of items in the
        list.

        :param action:  The QAction chosen by the user from the menu.
        """
        if action:
            # The action group containing these actions is set to
            # exclusive activation, so we're always dealing with a
            # checked action when this slot is called. We can just
            # set the primary sort field, sort, and move on.
            field = action.data() or "id"
            self.version_proxy_model.primary_sort_field = field

        self.version_proxy_model.sort(
            0,
            (QtCore.Qt.AscendingOrder
             if self._sort_versions_ascending else QtCore.Qt.DescendingOrder),
        )

        # We need to force a reselection after sorting. This will
        # remove edit widgets and allow a full repaint of the view,
        # and then reselect to go back to editing.
        self.version_delegate.force_reselection()

    ##########################################################################
    # fields menu filters

    def _checked_filter(self, field):
        """
        Checked filter method for the EntityFieldMenu. Determines whether the
        given field should be checked in the field menu.

        :param field:   The field name being processed.
        """
        return (field in self._active_fields)

    def _version_list_checked_filter(self, field):
        """
        Checked filter method for the EntityFieldMenu. Determines whether the
        given field should be checked in the field menu.

        :param field:   The field name being processed.
        """
        return (field in self.version_delegate.fields)

    def _disabled_filter(self, field):
        """
        Disabled filter method for the EntityFieldMenu. Determines whether the
        given field should be active or disabled in the field menu.

        :param field:   The field name being processed.
        """
        return (field in self._persistent_fields)

    def _version_list_disabled_filter(self, field):
        """
        Disabled filter method for the EntityFieldMenu. Determines whether the
        given field should be active or disabled in the field menu.

        :param field:   The field name being processed.
        """
        return (field in self._version_list_persistent_fields)

    def _field_filter(self, field):
        """
        Field filter method for the EntityFieldMenu. Determines whether the
        given field should be included in the field menu.

        :param field:   The field name being processed.
        """
        # Allow any fields that we have a widget available for.
        return bool(
            self.ui.current_version_card.field_manager.supported_fields(
                "Version",
                [field],
            ))
class ProgressDetailsWidget(QtGui.QWidget):
    """
    Progress reporting and logging
    """

    copy_to_clipboard_clicked = QtCore.Signal()

    def __init__(self, progress_widget, parent):
        """
        :param parent: The model parent.
        :type parent: :class:`~PySide.QtGui.QObject`
        """
        super(ProgressDetailsWidget, self).__init__(parent)

        self._bundle = sgtk.platform.current_bundle()

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

        self._progress_widget = progress_widget

        # hook up a listener to the parent window so this widget
        # follows along when the parent window changes size
        filter = ResizeEventFilter(parent)
        filter.resized.connect(self._on_parent_resized)
        parent.installEventFilter(filter)

        # dispatch clipboard signal
        self.ui.copy_log_button.clicked.connect(
            self.copy_to_clipboard_clicked.emit)

        self.ui.close.clicked.connect(self.toggle)

        # make sure the first column takes up as much space as poss.
        if self._bundle.engine.has_qt5:
            # see http://doc.qt.io/qt-5/qheaderview-obsolete.html#setResizeMode
            self.ui.log_tree.header().setSectionResizeMode(
                0, QtGui.QHeaderView.Stretch)
        else:
            self.ui.log_tree.header().setResizeMode(0,
                                                    QtGui.QHeaderView.Stretch)

        self.ui.log_tree.setIndentation(8)

        self.hide()

    def toggle(self):
        """
        Toggles visibility on and off
        """
        if self.isVisible():
            self.hide()
        else:
            self.show()

    def show(self):
        super(ProgressDetailsWidget, self).show()
        self.__recompute_position()
        self.ui.log_tree.expandAll()

    @property
    def log_tree(self):
        """
        The tree widget which holds the log items
        """
        return self.ui.log_tree

    def __recompute_position(self):
        """
        Adjust geometry of the widget based on progress widget
        """
        pos = self._progress_widget.pos()

        self.setGeometry(
            QtCore.QRect(pos.x(), 5, self._progress_widget.width(),
                         pos.y() - 5))

    def _on_parent_resized(self):
        """
        Special slot hooked up to the event filter.
        When associated widget is resized this slot is being called.
        """
        self.__recompute_position()
Example #15
0
    def setupUi(self, FileWidget):
        FileWidget.setObjectName("FileWidget")
        FileWidget.resize(291, 76)
        FileWidget.setStyleSheet("")
        self.horizontalLayout = QtGui.QHBoxLayout(FileWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.background = QtGui.QFrame(FileWidget)
        self.background.setStyleSheet(
            "#background {\n"
            "border-style: solid;\n"
            "border-width: 2px;\n"
            "border-radius: 2px;\n"
            "}\n"
            "\n"
            "#background[selected=false] {\n"
            "    background-color: rgb(0,0,0,0);\n"
            "    border-color: rgb(0,0,0,0);\n"
            "}\n"
            "\n"
            "#background[selected=true] {\n"
            "/*\n"
            "    background-color: rgb(135, 166, 185, 50);\n"
            "    border-color: rgb(135, 166, 185);\n"
            "*/\n"
            "    background-color: rgb(0, 178, 236, 30);\n"
            "    border-color: rgb(0, 178, 236);\n"
            "}")
        self.background.setFrameShape(QtGui.QFrame.StyledPanel)
        self.background.setFrameShadow(QtGui.QFrame.Plain)
        self.background.setLineWidth(2)
        self.background.setProperty("selected", True)
        self.background.setObjectName("background")
        self.horizontalLayout_2 = QtGui.QHBoxLayout(self.background)
        self.horizontalLayout_2.setContentsMargins(4, 4, 4, 4)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.thumbnail = QtGui.QLabel(self.background)
        self.thumbnail.setMinimumSize(QtCore.QSize(96, 64))
        self.thumbnail.setMaximumSize(QtCore.QSize(96, 64))
        self.thumbnail.setStyleSheet("")
        self.thumbnail.setText("")
        self.thumbnail.setTextFormat(QtCore.Qt.AutoText)
        self.thumbnail.setPixmap(
            QtGui.QPixmap(":/tk-multi-workfiles2/thumb_empty.png"))
        self.thumbnail.setScaledContents(True)
        self.thumbnail.setAlignment(QtCore.Qt.AlignCenter)
        self.thumbnail.setObjectName("thumbnail")
        self.horizontalLayout_2.addWidget(self.thumbnail)
        self.verticalLayout = QtGui.QVBoxLayout()
        self.verticalLayout.setSpacing(2)
        self.verticalLayout.setObjectName("verticalLayout")
        spacerItem = QtGui.QSpacerItem(20, 0, QtGui.QSizePolicy.Minimum,
                                       QtGui.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.label = ElidedLabel(self.background)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.subtitle = QtGui.QLabel(self.background)
        self.subtitle.setObjectName("subtitle")
        self.verticalLayout.addWidget(self.subtitle)
        spacerItem1 = QtGui.QSpacerItem(20, 0, QtGui.QSizePolicy.Minimum,
                                        QtGui.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem1)
        self.verticalLayout.setStretch(0, 1)
        self.verticalLayout.setStretch(3, 1)
        self.horizontalLayout_2.addLayout(self.verticalLayout)
        self.horizontalLayout_2.setStretch(1, 1)
        self.horizontalLayout.addWidget(self.background)

        self.retranslateUi(FileWidget)
        QtCore.QMetaObject.connectSlotsByName(FileWidget)
class PublishTreeWidget(QtGui.QTreeWidget):
    """
    Publish tree widget which contains context, summary, tasks and items.
    """

    # emitted when a status icon is clicked
    status_clicked = QtCore.Signal(object)
    # emitted when the tree has been rearranged using drag n drop
    tree_reordered = QtCore.Signal()
    # emitted when a checkbox has been clicked in the tree
    # passed the TreeNodeBase instance
    checked = QtCore.Signal(object)

    # keep a handle on all items created for the tree. the publisher is
    # typically a transient interface, so this shouldn't be an issue in terms of
    # sucking up memory. hopefully this will eliminate some of the
    # "Internal C++ object already deleted" errors we're seeing.
    __created_items = []

    def __init__(self, parent):
        """
        :param parent: The parent QWidget for this control
        """
        super(PublishTreeWidget, self).__init__(parent)
        self._plugin_manager = None
        self._selected_items_state = []
        self._bundle = sgtk.platform.current_bundle()
        # make sure that we cannot drop items on the root item
        self.invisibleRootItem().setFlags(QtCore.Qt.ItemIsEnabled)

        # 20 px indent for items
        self.setIndentation(28)
        # no indentation for the top level items
        self.setRootIsDecorated(False)
        # turn off keyboard focus - this is to disable the
        # dotted lines around the widget which is selected.
        self.setFocusPolicy(QtCore.Qt.NoFocus)

        # create the summary node and add to the tree
        self._summary_node = TreeNodeSummary(self)
        self.addTopLevelItem(self._summary_node)
        self._summary_node.setHidden(True)

        # forward double clicks on items to the items themselves
        self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c))

    def set_plugin_manager(self, plugin_manager):
        """
        Associate a plugin manager.

        This should be done once and immediately after
        construction. The reason it is not part of the constructor
        is to allow the widget to be used in QT Designer.

        :param plugin_manager: Plugin manager instance
        """
        self._plugin_manager = plugin_manager

    def _build_item_tree_r(self, item, enabled, level, tree_parent):
        """
        Build a subtree of items, recursively, for the given item

        :param item: Low level processing item instance
        :param bool enabled: flag to indicate that the item is enabled
        :param int level: recursion depth
        :param QTreeWidgetItem tree_parent: parent node in tree
        """
        if len(item.tasks) == 0 and len(item.children) == 0:
            # orphan. Don't create it
            return None

        if level == 0:
            ui_item = TopLevelTreeNodeItem(item, tree_parent)
        else:
            ui_item = TreeNodeItem(item, tree_parent)

        self.__created_items.append(ui_item)

        # set expand state for item
        ui_item.setExpanded(item.expanded)

        # see if the node is enabled. This setting propagates down
        enabled &= item.enabled

        # create children
        for task in item.tasks:
            task = TreeNodeTask(task, ui_item)
            self.__created_items.append(task)

        for child in item.children:
            self._build_item_tree_r(child, enabled, level+1, ui_item)

        # lastly, handle the item level check state.
        # if the item has been marked as checked=False
        # uncheck it now (which will affect all children)
        if not item.checked:
            ui_item.set_check_state(QtCore.Qt.Unchecked)

        return ui_item

    def build_tree(self):
        """
        Rebuilds the tree, ensuring that it is in sync with
        the low level plugin manager state.

        Does this in a lazy way in order to preserve as much
        state is possible.
        """

        logger.debug("Building tree.")

        # pass 1 - check if there is any top level item in the tree which shouldn't be there.
        #          also build a list of all top level items in the tree
        #          also check for items where the context has changed (e.g. so they need to move)
        #
        top_level_items_in_tree = []
        items_to_move = []
        for top_level_index in xrange(self.topLevelItemCount()):
            top_level_item = self.topLevelItem(top_level_index)

            if not isinstance(top_level_item, TreeNodeContext):
                # skip summary
                continue

            # go backwards so that when we take stuff out we don't
            # destroy the indices
            for item_index in reversed(range(top_level_item.childCount())):
                item = top_level_item.child(item_index)

                if item.item not in self._plugin_manager.top_level_items:
                    # no longer in the plugin mgr. remove from tree
                    top_level_item.takeChild(item_index)
                else:
                    # an active item
                    top_level_items_in_tree.append(item.item)
                    # check that items are parented under the right context
                    if str(item.item.context) != str(top_level_item.context):
                        # this object needs moving!
                        (item, state) = self.__take_item(top_level_item, item_index)
                        items_to_move.append((item, state))

        # now put all the moved items in
        for item, state in items_to_move:
            self.__insert_item(item, state)

        # pass 2 - check that there aren't any dangling contexts
        # process backwards so that when we take things out we don't
        # destroy the list
        for top_level_index in reversed(range(self.topLevelItemCount())):
            top_level_item = self.topLevelItem(top_level_index)

            if not isinstance(top_level_item, TreeNodeContext):
                # skip summary
                continue

            if top_level_item.childCount() == 0:
                self.takeTopLevelItem(top_level_index)

        # pass 3 - see if anything needs adding
        for item in self._plugin_manager.top_level_items:
            if item not in top_level_items_in_tree:
                self.__add_item(item)

        # finally, see if we should show the summary widget or not
        if len(self._plugin_manager.top_level_items) < 2:
            self._summary_node.setHidden(True)
        else:
            self._summary_node.setHidden(False)

    def __ensure_context_node_exists(self, context):
        """
        Make sure a node representing the context exists in the tree

        :param context: Toolkit context
        :returns: context item object
        """
        # first find the right context
        context_tree_node = None
        for context_index in xrange(self.topLevelItemCount()):
            context_item = self.topLevelItem(context_index)
            if isinstance(context_item, TreeNodeContext) and str(context_item.context) == str(context):
                context_tree_node = context_item

        if context_tree_node is None:
            # context not found! Create it!
            context_tree_node = TreeNodeContext(context, self)
            self.__created_items.append(context_tree_node)
            context_tree_node.setExpanded(True)
            self.addTopLevelItem(context_tree_node)

        return context_tree_node

    def __get_item_state(self, item):
        """
        Extract the state for the given tree item.
        Use :meth:`__set_item_state` to apply the returned data.

        :param item: Item to operate on
        :returns: dict with state
        """
        state = {
            "selected": item.isSelected(),
            "expanded": item.isExpanded()
        }
        return state

    def __set_item_state(self, item, state):
        """
        Applies state previously extracted with :meth:`__get_item_state` to an item.

        :param item: Item to operate on
        :param state: State dictionary to apply, as returned by :meth:`__get_item_state`
        """
        item.setSelected(state["selected"])
        item.setExpanded(state["expanded"])

    def __take_item(self, parent, index):
        """
        Takes out the given widget out of the tree
        and captures its state. This is meant to be used
        in conjunction with :meth:`__insert_item`.

        :param parent: parent item
        :param index: index of the item to take out
        :returns: (item, state)
        :rtype: (QTreeWidgetItem, dict)
        """
        state = self.__get_item_state(parent.child(index))
        item = parent.takeChild(index)
        return item, state

    def __insert_item(self, widget_item, state):
        """
        Inserts the given item into the tree

        :param widget_item: item to put in
        :param dict state: state dictionary as created by :meth:`__take_item`.
        """
        context_tree_node = self.__ensure_context_node_exists(widget_item.item.context)
        context_tree_node.addChild(widget_item)

        # re-initialize the item recursively
        _init_item_r(widget_item)

        # restore its state
        self.__set_item_state(widget_item, state)

        # update the task statuses for this item
        self.update_tasks_for_item(widget_item)

        # if the item is selected, scroll to it after the move
        if widget_item.isSelected():
            widget_item.treeWidget().scrollToItem(widget_item)

    def __add_item(self, processing_item):
        """
        Create a node in the tree to represent the given top level item

        :param processing_item: processing module item instance.
        """
        context_tree_node = self.__ensure_context_node_exists(processing_item.context)

        # now add the new node
        self._build_item_tree_r(
            processing_item,
            enabled=True,
            level=0,
            tree_parent=context_tree_node
        )

    def get_full_summary(self):
        """
        Compute a full summary report.

        :returns: (num_items, string with html)
        """
        summary = []
        num_items = 0
        for context_index in xrange(self.topLevelItemCount()):
            context_item = self.topLevelItem(context_index)
            summary.extend(context_item.create_summary())
            tasks = self._summarize_tasks_r(context_item)

            # values of the tasks dictionary contains
            # how many items there are for each type
            num_items += sum(tasks.values())
            # iterate over dictionary and build histogram
            for task_name, num_tasks in tasks.iteritems():
                if num_tasks == 1:
                    summary.append("&ndash; %s: 1 item<br>" % task_name)
                else:
                    summary.append("&ndash; %s: %s items<br>" % (task_name, num_tasks))

        if len(summary) == 0:
            summary_text = "Nothing will published."

        else:
            summary_text = "".join(["%s" % line for line in summary])

        return (num_items, summary_text)

    def _summarize_tasks_r(self, node):
        """
        Recurses down and counts tasks

        :param node: The root node to begin recursion from
        :returns: Dictionary keyed by task name and where the
            value represents the number of instances of that task.
        """
        tasks = defaultdict(int)
        for child_index in xrange(node.childCount()):
            child = node.child(child_index)
            if isinstance(child, TreeNodeTask) and child.enabled:
                task_obj = child.task
                tasks[task_obj.plugin.name] += 1
            else:
                # process children
                child_tasks = self._summarize_tasks_r(child)
                for task_name, num_task_instances in child_tasks.iteritems():
                    tasks[task_name] += num_task_instances
        return tasks

    def select_first_item(self):
        """
        Selects the summary if it exists,
        otherwise selects he first item in the tree.
        """
        self.clearSelection()

        logger.debug("Selecting first item in the tree..")
        if self.topLevelItemCount() == 0:
            logger.debug("Nothing to select!")
            return

        # summary item is always the first one
        summary_item = self.topLevelItem(0)
        if not summary_item.isHidden():
            logger.debug("Selecting the summary node")
            self.setCurrentItem(summary_item)

        else:
            # summary hidden. select first item instead.
            first_item = None
            for context_index in xrange(1, self.topLevelItemCount()):
                context_item = self.topLevelItem(context_index)
                for child_index in xrange(context_item.childCount()):
                    first_item = context_item.child(child_index)
                    break
            if first_item:
                self.setCurrentItem(first_item)
                logger.debug("No summary node present. Selecting %s" % first_item)
            else:
                logger.debug("Nothing to select!")

    def set_check_state_for_all_plugins(self, plugin, state):
        """
        Set the check state for all items associated with the given plugin

        :param plugin: Plugin for which tasks should be manipulated
        :param state: checkstate to set.
        """
        logger.debug(
            "Setting state %d for all plugin %s" % (state, plugin)
        )

        def _check_r(parent):
            for child_index in xrange(parent.childCount()):
                child = parent.child(child_index)
                if isinstance(child, TreeNodeTask) and child.task.plugin == plugin:
                    child.set_check_state(state)
                _check_r(child)
        root = self.invisibleRootItem()
        _check_r(root)

    def update_tasks_for_item(self, item):
        """
        Sync the states for all item's tasks with their associated data

        :param item: Widget item for which tasks should be updated
        """
        def _check_r(parent):
            for child_index in xrange(parent.childCount()):
                child = parent.child(child_index)
                if isinstance(child, TreeNodeTask):
                    child.set_check_state(child.task.checked)
                    child.set_check_enabled(child.task.enabled)
                _check_r(child)
        _check_r(item)

    def dropEvent(self, event):
        """
        Something was dropped on this widget
        """

        # run default implementation
        super(PublishTreeWidget, self).dropEvent(event)

        for item, state in self._selected_items_state:

            # make sure that the item picks up the new context
            if isinstance(item, TopLevelTreeNodeItem):
                item.synchronize_context()

            # re-initialize the item recursively
            _init_item_r(item)

            # restore state after drop
            self.__set_item_state(item, state)

        self.tree_reordered.emit()

    def dragEnterEvent(self, event):
        """
        Event triggering when a drag operation starts
        """
        # record selection for use later.
        self._selected_items_state = []

        dragged_items = []
        selected_items_state = []

        # process all selected items
        for item in self.selectedItems():

            # we only want to drag/drop top level items
            if isinstance(item, TopLevelTreeNodeItem):

                # ensure context change is allowed before dragging/dropping
                if not item.item.context_change_allowed:
                    # deselect to prevent drag/drop of items whose context
                    # can't be changed
                    item.setSelected(False)

                dragged_items.append(item)

            else:
                # deselect to prevent drag/drop of non top level items
                item.setSelected(False)

            # keep the state of all selected items to restore post-drop
            state = self.__get_item_state(item)
            selected_items_state.append((item, state))

        # ignore any selection that does not contain at least one TopLevelTreeNodeItems
        if not dragged_items:
            logger.debug("No top-level nodes included in selection.")        
            return

        self._selected_items_state = selected_items_state

        super(PublishTreeWidget, self).dragEnterEvent(event)

    def mouseMoveEvent(self, event):
        """
        Overridden mouse move event to suppress
        selecting multiple selection via the mouse since
        this makes drag and drop pretty weird.
        """
        if self.state() != QtGui.QAbstractItemView.DragSelectingState:
            # bubble up all events that aren't drag select related
            super(PublishTreeWidget, self).mouseMoveEvent(event)
Example #17
0
class GlobalSearchCompleter(SearchCompleter):
    """
    A standalone :class:`PySide.QtGui.QCompleter` class for matching SG entities to typed text.

    :signal: ``entity_selected(str, int)`` - Provided for backward compatibility.
      ``entity_activated`` is emitted at the same time with an additional ``name``
      value. Fires when someone selects an entity inside the search results. The
      returned parameters are entity ``type`` and entity ``id``.

    :signal: ``entity_activated(str, int, str)`` - Fires when someone activates an
      entity inside the search results. Essentially the same as ``entity_selected``
      only the parameters returned are ``type``, ``id`` **and** ``name``.

    :modes: ``MODE_LOADING, MODE_NOT_FOUND, MODE_RESULT`` - Used to identify the
        mode of an item in the completion list

    :model role: ``MODE_ROLE`` - Stores the mode of an item in the completion
        list (see modes above)

    :model role: ``SG_DATA_ROLE`` - Role for storing shotgun data in the model
    """

    # emitted when shotgun has been updated
    entity_selected = QtCore.Signal(str, int)
    entity_activated = QtCore.Signal(str, int, str)

    def __init__(self, parent=None):
        """
        :param parent: Parent widget
        :type parent: :class:`~PySide.QtGui.QWidget`
        """
        super(GlobalSearchCompleter, self).__init__(parent)

        # the default entity search criteria. The calling code can override these
        # criterial by calling ``set_searchable_entity_types``.
        self._entity_search_criteria = {
            "Asset": [],
            "Shot": [],
            "Task": [],
            "HumanUser": [["sg_status_list", "is", "act"]],
            "Group": [],
            "ClientUser": [["sg_status_list", "is", "act"]],
            "ApiUser": [],
            "Version": [],
            "PublishedFile": [],
        }

    def get_result(self, model_index):
        """
        Return the entity data for the supplied model index or None if there is
        no data for the supplied index.

        :param model_index: The index of the model to return the result for.
        :type model_index: :class:`~PySide.QtCore.QModelIndex`

        :return: The entity dict for the supplied model index.
        :rtype: :obj:`dict`: or ``None``
        """

        # make sure that the user selected an actual shotgun item.
        # if they just selected the "no items found" or "loading please hold"
        # items, just ignore it.
        mode = shotgun_model.get_sanitized_data(model_index, self.MODE_ROLE)
        if mode == self.MODE_RESULT:

            # get the payload
            data = shotgun_model.get_sanitized_data(model_index,
                                                    self.SG_DATA_ROLE)

            # Example of data stored in the data role:
            #
            # {'status': 'vwd',
            #  'name': 'bunny_010_0050_comp_v001',
            #  'links': ['Shot', 'bunny_010_0050'],
            #  'image': 'https://xxx',
            #  'project_id': 65,
            #  'type': 'Version',
            #  'id': 99}

            # NOTE: this data format differs from what is typically returned by
            # the shotgun python-api. this data may be formalized at some point
            # but for now, only expose the minimum information.

            return {
                "type": data["type"],
                "id": data["id"],
                "name": data["name"],
            }
        else:
            return None

    def set_searchable_entity_types(self, types_dict):
        """
        Specify a dictionary of entity types with optional search filters to
        limit the breadth of the widget's search.

        Use this method to override the default searchable entity types
        dictionary which looks like this::

          {
            "Asset": [],
            "Shot": [],
            "Task": [],
            "HumanUser": [["sg_status_list", "is", "act"]],    # only active users
            "Group": [],
            "ClientUser": [["sg_status_list", "is", "act"]],   # only active users
            "ApiUser": [],
            "Version": [],
            "PublishedFile": [],
          }

        :param types_dict: A dictionary of searchable types with optional filters

        """
        self._entity_search_criteria = types_dict

    def _set_item_delegate(self, popup, text):
        """
        Sets an item delegate for the completer's popup.

        :param popup: Popup instance from the completer.
        :type popup: :class:`~PySide.QtGui.QAbstractItemView`

        :param str text: Text used for completion.
        """
        # deferred import to help documentation generation.
        from .global_search_result_delegate import GlobalSearchResultDelegate
        self._delegate = GlobalSearchResultDelegate(popup, text)
        popup.setItemDelegate(self._delegate)

    def _launch_sg_search(self, text):
        """
        Launches a search on the Shotgun server.

        :param str text: Text to search for.

        :returns: The :class:`~tk-framework-shotgunutils:shotgun_data.ShotgunDataRetriever`'s job id.
        """

        # constrain by project in the search
        project_ids = []

        if len(self._entity_search_criteria.keys()) == 1 and \
           "Project" in self._entity_search_criteria:
            # this is a Project-only search. don't restrict by the current project id
            pass
        elif self._bundle.context.project:
            project_ids.append(self._bundle.context.project["id"])

        return self._sg_data_retriever.execute_text_search(
            text, self._entity_search_criteria, project_ids)

    def _on_select(self, model_index):
        """
        Fires when an item in the completer is selected. This will emit an entity_selected signal
        for the global search widget

        :param model_index: QModelIndex describing the current item
        """
        data = self.get_result(model_index)
        if data:
            self.entity_selected.emit(data["type"], data["id"])
            self.entity_activated.emit(data["type"], data["id"], data["name"])

    def _handle_search_results(self, data):
        """
        Populates the model associated with the completer with the data coming back from Shotgun.

        :param dict data: Data received back from the job sent to the
            :class:`~tk-framework-shotgunutils:shotgun_data.ShotgunDataRetriever` in :method:``_launch_sg_search``.
        """
        matches = data["sg"]["matches"]

        if len(matches) == 0:
            item = QtGui.QStandardItem("No matches found!")
            item.setData(self.MODE_NOT_FOUND, self.MODE_ROLE)
            self.model().appendRow(item)

        # insert new data into model
        for d in matches:
            item = QtGui.QStandardItem(d["name"])
            item.setData(self.MODE_RESULT, self.MODE_ROLE)

            item.setData(shotgun_model.sanitize_for_qt_model(d),
                         self.SG_DATA_ROLE)

            item.setIcon(self._pixmaps.no_thumbnail)

            if d.get("image") and self._sg_data_retriever:
                uid = self._sg_data_retriever.request_thumbnail(
                    d["image"], d["type"], d["id"], "image", load_image=True)
                self._thumb_map[uid] = {"item": item}

            self.model().appendRow(item)
Example #18
0
class ReplyDialog(QtGui.QDialog):
    """
    Modal dialog that hosts a note reply widget.
    This is used when someone clicks on the reply button for a note.
    """
    def __init__(self,
                 parent,
                 bg_task_manager,
                 note_id=None,
                 allow_screenshots=True):
        """
        :param parent: QT parent object
        :type parent: :class:`PySide.QtGui.QWidget`
        :param bg_task_manager: Task manager to use to fetch sg data.
        :type  bg_task_manager: :class:`~tk-framework-shotgunutils:task_manager.BackgroundTaskManager`
        :param note_id: The entity id number of the Note entity being replied to.
        :type  note_id: :class:`int`
        :param allow_screenshots: Boolean to allow or disallow screenshots, defaults to True.
        :type  allow_screenshots: :class:`Boolean`
        """
        # first, call the base class and let it do its thing.
        QtGui.QDialog.__init__(self, parent)

        # now load in the UI that was created in the UI designer
        self.ui = Ui_ReplyDialog()
        self.ui.setupUi(self)

        self._note_id = note_id

        self.ui.note_widget.set_bg_task_manager(bg_task_manager)
        self.ui.note_widget.data_updated.connect(self.close_after_create)
        self.ui.note_widget.close_clicked.connect(self.close_after_cancel)
        self.ui.note_widget.set_current_entity("Note", note_id)
        self.ui.note_widget.allow_screenshots(allow_screenshots)

        self.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.FramelessWindowHint)

    @property
    def note_widget(self):
        """
        Returns the underlying :class:`~note_input_widget.NoteInputWidget`.
        """
        return self.ui.note_widget

    def _get_note_id(self):
        """
        Gets the entity id number for the parent Note entity
        being replied to.

        :returns:   int
        """
        return self._note_id

    def _set_note_id(self, note_id):
        """
        Sets the entity id number for the parent Note entity
        being replied to.

        :param note_id: Integer id of a Note entity in Shotgun.
        """
        self.ui.note_widget.set_current_entity("Note", note_id)
        self._note_id = note_id

    note_id = QtCore.Property(int, _get_note_id, _set_note_id)

    def close_after_create(self):
        """
        Callback called after successful reply
        """
        self.setResult(QtGui.QDialog.Accepted)
        self.close()

    def close_after_cancel(self):
        """
        Callback called after cancel
        """
        self.setResult(QtGui.QDialog.Rejected)
        self.close()

    def showEvent(self, event):
        QtGui.QDialog.showEvent(self, event)
        self.ui.note_widget.open_editor()

    def closeEvent(self, event):
        self.ui.note_widget.clear()
        # ok to close
        event.accept()
Example #19
0
class UpdateProjectConfig(QtGui.QWidget):
    update_finished = QtCore.Signal(bool)

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

        # initialize variables
        self.python_interpreter_path = None
        self.core_python_path = None
        self.configuration_path = None
        self.project = None

        # setup the gui
        self.ui = update_project_config.Ui_UpdateProjectConfig()
        self.ui.setupUi(self)
        self.ui.button.clicked.connect(self.do_config)
        self._parent = parent

        # hide outcome messages
        self.ui.success.setVisible(False)

        # resize with parent
        filter = ResizeEventFilter(self._parent)
        filter.resized.connect(self._on_parent_resized)
        self._parent.installEventFilter(filter)

        # start off hidden
        self.setVisible(False)

    def set_project_info(self, path_to_python, core_python, config_path,
                         project):
        # keep the info
        self.python_interpreter_path = path_to_python
        self.core_python_path = core_python
        self.configuration_path = config_path
        self.project = project

        # reset the ui
        self.ui.label.setVisible(True)
        self.ui.success.setVisible(False)
        self.ui.button.setEnabled(True)

    def do_config(self):
        # disable the button
        self.ui.button.setEnabled(False)

        # figure out path to script to execute
        apps_copy_script = os.path.realpath(
            os.path.join(__file__, "..", "..", "..",
                         "add_desktop_to_project.py"))

        # put together the arguments to the command
        args = [
            self.python_interpreter_path,
            apps_copy_script,
            "--core_python_path",
            self.core_python_path,
            "--configuration_path",
            self.configuration_path,
            "--project_id",
            str(self.project["id"]),
        ]

        try:
            # show a message to indicate that something is happening
            wait = WaitScreen("Updating project config,", "hold on...", self)
            wait.show()
            QtGui.QApplication.instance().processEvents()

            # run hidden on windows
            startupinfo = None
            if sys.platform == "win32":
                startupinfo = subprocess.STARTUPINFO()
                startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
                startupinfo.wShowWindow = subprocess.SW_HIDE

            # call it
            python_process = subprocess.Popen(args,
                                              stderr=subprocess.PIPE,
                                              stdout=subprocess.PIPE,
                                              stdin=subprocess.PIPE,
                                              startupinfo=startupinfo)
            (stdout, stderr) = python_process.communicate()
        finally:
            # make sure the wait screen gets hidden
            wait.hide()

        if python_process.returncode == 0:
            # success
            self.ui.label.setVisible(False)
            self.ui.success.setVisible(True)
            self.update_finished.emit(True)
        else:
            # failure
            message = """
                <html><head/><body>
                    <p><span style=" font-size:16pt;">
                        There was an error adding the desktop engine:
                    </span></p>
                    <p>
                        <pre>%s</pre>
                    </p>
                    <p><span style=" font-size:14pt;">
                        Please let [email protected] know.
                    </span></p>
                </body></html>
            """ % stderr

            # show the error
            e = ErrorDialog("Toolkit Error", message, self)
            e.exec_()

            # set the button up for another attempt
            self.ui.button.setText("Try Again")
            self.ui.button.setEnabled(True)

            # let listeners know we finished
            self.update_finished.emit(False)

    def _on_parent_resized(self):
        """
        Special slot hooked up to the event filter.
        When associated widget is resized this slot is being called.
        """
        # resize overlay
        self.resize(self._parent.size())
Example #20
0
class SetupProject(QtGui.QWidget):
    setup_finished = QtCore.Signal(bool)

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

        self.ui = setup_project.Ui_SetupProject()
        self.ui.setupUi(self)
        self.ui.button.clicked.connect(self.do_setup)
        self._parent = parent
        self.project = None

        filter = ResizeEventFilter(self._parent)
        filter.resized.connect(self._on_parent_resized)
        self._parent.installEventFilter(filter)

        self.setVisible(False)

    def do_setup(self, show_help=False):
        try:
            # First check to see if the current user
            self._validate_user_permissions()

            if show_help:
                # Display the Help popup if requested.
                self.show_help_popup()

            setup = adminui.SetupProjectWizard(self.project, self)

            ret = setup.exec_()
            self.setup_finished.emit(ret == setup.Accepted)

        except TankErrorProjectIsSetup as e:
            error_dialog = ErrorDialog(
                "Toolkit Setup Error",
                "You are trying to set up a project which has already been set up\n\n"
                "To re-setup a project, in a terminal window type: tank setup_project --force\n\n"
                "Alternatively, you can go into shotgun and clear the Project.tank_name field\n"
                "and delete all pipeline configurations for your project.",
            )
            error_dialog.exec_()

        except TankUserPermissionsError as e:
            error_dialog = ErrorDialog(
                "Toolkit Setup Error",
                "You do not have sufficient permissions in ShotGrid to setup Toolkit for "
                "project '%s'.\n\nContact a site administrator for assistance."
                % self.project["name"],
            )
            error_dialog.exec_()

    def show_help_popup(self):
        """
        Display a help screen
        """
        # For the interim, just launch an information MessageBox
        # that will open a link to the Toolkit Project setup wizard
        # documentation
        help_text = (
            "Find out more about the Setup Project Wizard by " "clicking 'Open' below."
        )
        help_buttons = QtGui.QMessageBox.Open | QtGui.QMessageBox.Cancel
        user_input = QtGui.QMessageBox.information(
            self, "Setup Project Help", help_text, help_buttons, QtGui.QMessageBox.Open
        )

        if user_input == QtGui.QMessageBox.Open:
            # Go to the Toolkit Project setup wizard documentation
            help_url = "https://developer.shotgridsoftware.com/5d83a936/?title=Configuration+Setup"
            QtGui.QDesktopServices.openUrl(help_url)

    def _validate_user_permissions(self):
        """
        Attempt to modify the Project's tank_name field to determine whether
        the current user has sufficient permission to setup the Project's
        pipeline configuration.
        """
        try:
            # Try to update the Project's tank_name value in SG to test
            # whether current user has sufficient permission to setup
            # Toolkit for a project.
            engine = sgtk.platform.current_engine()
            sg_project = engine.shotgun.find_one(
                "Project", [["id", "is", self.project["id"]]], ["tank_name"]
            )
            engine.shotgun.update(
                "Project", self.project["id"], {"tank_name": "foobar"}
            )
            engine.shotgun.update(
                "Project", self.project["id"], {"tank_name": sg_project["tank_name"]}
            )
        except Exception as e:
            # Attempting to catch a shotgun_api3.Fault here using 'except Fault:'
            # just passes through, so we need to catch the general Exception instead
            # and check the error message directly for the specific problems we want
            # to handle.
            if "field is not editable for this user" in str(e):
                # Insufficient user permissions to setup Toolkit for a project.
                raise TankUserPermissionsError(e)

            # Raise any other general Exceptions.
            raise

    def _on_parent_resized(self):
        """
        Special slot hooked up to the event filter.
        When associated widget is resized this slot is being called.
        """
        # resize overlay
        self.resize(self._parent.size())
    def setupUi(self, FileGroupWidget):
        FileGroupWidget.setObjectName("FileGroupWidget")
        FileGroupWidget.resize(326, 57)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.MinimumExpanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            FileGroupWidget.sizePolicy().hasHeightForWidth())
        FileGroupWidget.setSizePolicy(sizePolicy)
        FileGroupWidget.setMouseTracking(True)
        FileGroupWidget.setFocusPolicy(QtCore.Qt.TabFocus)
        self.verticalLayout = QtGui.QVBoxLayout(FileGroupWidget)
        self.verticalLayout.setSpacing(2)
        self.verticalLayout.setSizeConstraint(QtGui.QLayout.SetMinimumSize)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout = QtGui.QHBoxLayout()
        self.horizontalLayout.setContentsMargins(-1, -1, 6, -1)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.expand_check_box = QtGui.QCheckBox(FileGroupWidget)
        self.expand_check_box.setMinimumSize(QtCore.QSize(0, 20))
        self.expand_check_box.setStyleSheet("")
        self.expand_check_box.setText("")
        self.expand_check_box.setIconSize(QtCore.QSize(16, 16))
        self.expand_check_box.setObjectName("expand_check_box")
        self.horizontalLayout.addWidget(self.expand_check_box)
        self.title_label = QtGui.QLabel(FileGroupWidget)
        self.title_label.setStyleSheet("")
        self.title_label.setAlignment(QtCore.Qt.AlignLeading
                                      | QtCore.Qt.AlignLeft
                                      | QtCore.Qt.AlignVCenter)
        self.title_label.setMargin(0)
        self.title_label.setObjectName("title_label")
        self.horizontalLayout.addWidget(self.title_label)
        self.user_label = QtGui.QLabel(FileGroupWidget)
        self.user_label.setStyleSheet("")
        self.user_label.setAlignment(QtCore.Qt.AlignLeading
                                     | QtCore.Qt.AlignLeft
                                     | QtCore.Qt.AlignVCenter)
        self.user_label.setIndent(3)
        self.user_label.setObjectName("user_label")
        self.horizontalLayout.addWidget(self.user_label)
        spacerItem = QtGui.QSpacerItem(0, 20, QtGui.QSizePolicy.Expanding,
                                       QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.spinner = QtGui.QLabel(FileGroupWidget)
        self.spinner.setMinimumSize(QtCore.QSize(20, 20))
        self.spinner.setMaximumSize(QtCore.QSize(20, 20))
        self.spinner.setStyleSheet("")
        self.spinner.setText("")
        self.spinner.setObjectName("spinner")
        self.horizontalLayout.addWidget(self.spinner)
        self.horizontalLayout.setStretch(3, 1)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.verticalLayout_2 = QtGui.QVBoxLayout()
        self.verticalLayout_2.setSpacing(0)
        self.verticalLayout_2.setSizeConstraint(QtGui.QLayout.SetMinimumSize)
        self.verticalLayout_2.setContentsMargins(0, -1, 0, -1)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.line = QtGui.QFrame(FileGroupWidget)
        self.line.setFrameShape(QtGui.QFrame.HLine)
        self.line.setFrameShadow(QtGui.QFrame.Sunken)
        self.line.setObjectName("line")
        self.verticalLayout_2.addWidget(self.line)
        self.msg_label = QtGui.QLabel(FileGroupWidget)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.msg_label.sizePolicy().hasHeightForWidth())
        self.msg_label.setSizePolicy(sizePolicy)
        self.msg_label.setStyleSheet("")
        self.msg_label.setAlignment(QtCore.Qt.AlignLeading
                                    | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
        self.msg_label.setWordWrap(True)
        self.msg_label.setMargin(2)
        self.msg_label.setObjectName("msg_label")
        self.verticalLayout_2.addWidget(self.msg_label)
        self.verticalLayout.addLayout(self.verticalLayout_2)
        self.verticalLayout.setStretch(1, 1)

        self.retranslateUi(FileGroupWidget)
        QtCore.QMetaObject.connectSlotsByName(FileGroupWidget)
Example #22
0
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(1226, 782)
        self.horizontalLayout_3 = QtGui.QHBoxLayout(Dialog)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.left_area = QtGui.QVBoxLayout()
        self.left_area.setSpacing(2)
        self.left_area.setObjectName("left_area")
        self.top_toolbar = QtGui.QHBoxLayout()
        self.top_toolbar.setObjectName("top_toolbar")
        self.navigation_home = QtGui.QToolButton(Dialog)
        self.navigation_home.setMinimumSize(QtCore.QSize(40, 40))
        self.navigation_home.setMaximumSize(QtCore.QSize(40, 40))
        self.navigation_home.setStyleSheet(
            "QToolButton{\n"
            "   border: none;\n"
            "   background-color: none;\n"
            "   background-repeat: no-repeat;\n"
            "   background-position: center center;\n"
            "   background-image: url(:/res/home.png);\n"
            "}\n"
            "\n"
            "QToolButton:hover{\n"
            "background-image: url(:/res/home_hover.png);\n"
            "}\n"
            "\n"
            "QToolButton:Pressed {\n"
            "background-image: url(:/res/home_pressed.png);\n"
            "}\n"
            "")
        self.navigation_home.setObjectName("navigation_home")
        self.top_toolbar.addWidget(self.navigation_home)
        self.navigation_prev = QtGui.QToolButton(Dialog)
        self.navigation_prev.setMinimumSize(QtCore.QSize(40, 40))
        self.navigation_prev.setMaximumSize(QtCore.QSize(40, 40))
        self.navigation_prev.setStyleSheet(
            "QToolButton{\n"
            "   border: none;\n"
            "   background-color: none;\n"
            "   background-repeat: no-repeat;\n"
            "   background-position: center center;\n"
            "   background-image: url(:/res/left_arrow.png);\n"
            "}\n"
            "\n"
            "QToolButton:disabled{\n"
            "   background-image: url(:/res/left_arrow_disabled.png);\n"
            "}\n"
            "\n"
            "QToolButton:hover{\n"
            "background-image: url(:/res/left_arrow_hover.png);\n"
            "}\n"
            "\n"
            "QToolButton:Pressed {\n"
            "background-image: url(:/res/left_arrow_pressed.png);\n"
            "}\n"
            "")
        self.navigation_prev.setObjectName("navigation_prev")
        self.top_toolbar.addWidget(self.navigation_prev)
        self.navigation_next = QtGui.QToolButton(Dialog)
        self.navigation_next.setMinimumSize(QtCore.QSize(40, 40))
        self.navigation_next.setMaximumSize(QtCore.QSize(40, 40))
        self.navigation_next.setStyleSheet(
            "QToolButton{\n"
            "   border: none;\n"
            "   background-color: none;\n"
            "   background-repeat: no-repeat;\n"
            "   background-position: center center;\n"
            "   background-image: url(:/res/right_arrow.png);\n"
            "}\n"
            "\n"
            "QToolButton:disabled{\n"
            "   background-image: url(:/res/right_arrow_disabled.png);\n"
            "}\n"
            "\n"
            "\n"
            "QToolButton:hover{\n"
            "background-image: url(:/res/right_arrow_hover.png);\n"
            "}\n"
            "\n"
            "QToolButton:Pressed {\n"
            "background-image: url(:/res/right_arrow_pressed.png);\n"
            "}\n"
            "")
        self.navigation_next.setObjectName("navigation_next")
        self.top_toolbar.addWidget(self.navigation_next)
        self.label = QtGui.QLabel(Dialog)
        self.label.setText("")
        self.label.setObjectName("label")
        self.top_toolbar.addWidget(self.label)
        self.left_area.addLayout(self.top_toolbar)
        self.entity_preset_tabs = QtGui.QTabWidget(Dialog)
        self.entity_preset_tabs.setMaximumSize(QtCore.QSize(300, 16777202))
        self.entity_preset_tabs.setUsesScrollButtons(True)
        self.entity_preset_tabs.setObjectName("entity_preset_tabs")
        self.left_area.addWidget(self.entity_preset_tabs)
        self.label_4 = QtGui.QLabel(Dialog)
        self.label_4.setAlignment(QtCore.Qt.AlignCenter)
        self.label_4.setObjectName("label_4")
        self.left_area.addWidget(self.label_4)
        self.publish_type_list = QtGui.QListView(Dialog)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.Maximum)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.publish_type_list.sizePolicy().hasHeightForWidth())
        self.publish_type_list.setSizePolicy(sizePolicy)
        self.publish_type_list.setMinimumSize(QtCore.QSize(100, 100))
        self.publish_type_list.setStyleSheet(
            "QListView::item {\n"
            "    border-top: 1px dotted #888888;\n"
            "    padding: 5px;\n"
            " }")
        self.publish_type_list.setEditTriggers(
            QtGui.QAbstractItemView.NoEditTriggers)
        self.publish_type_list.setProperty("showDropIndicator", False)
        self.publish_type_list.setSelectionMode(
            QtGui.QAbstractItemView.NoSelection)
        self.publish_type_list.setUniformItemSizes(True)
        self.publish_type_list.setObjectName("publish_type_list")
        self.left_area.addWidget(self.publish_type_list)
        self.horizontalLayout_6 = QtGui.QHBoxLayout()
        self.horizontalLayout_6.setSpacing(2)
        self.horizontalLayout_6.setObjectName("horizontalLayout_6")
        self.check_all = QtGui.QToolButton(Dialog)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.check_all.sizePolicy().hasHeightForWidth())
        self.check_all.setSizePolicy(sizePolicy)
        self.check_all.setMinimumSize(QtCore.QSize(60, 26))
        self.check_all.setObjectName("check_all")
        self.horizontalLayout_6.addWidget(self.check_all)
        self.check_none = QtGui.QToolButton(Dialog)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.check_none.sizePolicy().hasHeightForWidth())
        self.check_none.setSizePolicy(sizePolicy)
        self.check_none.setMinimumSize(QtCore.QSize(75, 26))
        self.check_none.setObjectName("check_none")
        self.horizontalLayout_6.addWidget(self.check_none)
        self.label_3 = QtGui.QLabel(Dialog)
        self.label_3.setText("")
        self.label_3.setAlignment(QtCore.Qt.AlignRight
                                  | QtCore.Qt.AlignTrailing
                                  | QtCore.Qt.AlignVCenter)
        self.label_3.setObjectName("label_3")
        self.horizontalLayout_6.addWidget(self.label_3)
        self.cog_button = QtGui.QToolButton(Dialog)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap(":/res/gear.png"), QtGui.QIcon.Normal,
                       QtGui.QIcon.Off)
        self.cog_button.setIcon(icon)
        self.cog_button.setIconSize(QtCore.QSize(20, 16))
        self.cog_button.setPopupMode(QtGui.QToolButton.InstantPopup)
        self.cog_button.setObjectName("cog_button")
        self.horizontalLayout_6.addWidget(self.cog_button)
        self.left_area.addLayout(self.horizontalLayout_6)
        self.horizontalLayout_3.addLayout(self.left_area)
        self.middle_area = QtGui.QVBoxLayout()
        self.middle_area.setObjectName("middle_area")
        self.horizontalLayout_2 = QtGui.QHBoxLayout()
        self.horizontalLayout_2.setSpacing(1)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.entity_breadcrumbs = QtGui.QLabel(Dialog)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Ignored,
                                       QtGui.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.entity_breadcrumbs.sizePolicy().hasHeightForWidth())
        self.entity_breadcrumbs.setSizePolicy(sizePolicy)
        self.entity_breadcrumbs.setMinimumSize(QtCore.QSize(0, 40))
        self.entity_breadcrumbs.setText("")
        self.entity_breadcrumbs.setObjectName("entity_breadcrumbs")
        self.horizontalLayout_2.addWidget(self.entity_breadcrumbs)
        spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Ignored,
                                       QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_2.addItem(spacerItem)
        self.thumbnail_mode = QtGui.QToolButton(Dialog)
        self.thumbnail_mode.setMinimumSize(QtCore.QSize(0, 26))
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap(":/res/mode_switch_thumb_active.png"),
                        QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.thumbnail_mode.setIcon(icon1)
        self.thumbnail_mode.setCheckable(True)
        self.thumbnail_mode.setChecked(True)
        self.thumbnail_mode.setObjectName("thumbnail_mode")
        self.horizontalLayout_2.addWidget(self.thumbnail_mode)
        self.list_mode = QtGui.QToolButton(Dialog)
        self.list_mode.setMinimumSize(QtCore.QSize(26, 26))
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap(":/res/mode_switch_card.png"),
                        QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.list_mode.setIcon(icon2)
        self.list_mode.setCheckable(True)
        self.list_mode.setObjectName("list_mode")
        self.horizontalLayout_2.addWidget(self.list_mode)
        self.label_5 = QtGui.QLabel(Dialog)
        self.label_5.setMinimumSize(QtCore.QSize(5, 0))
        self.label_5.setMaximumSize(QtCore.QSize(5, 16777215))
        self.label_5.setText("")
        self.label_5.setObjectName("label_5")
        self.horizontalLayout_2.addWidget(self.label_5)
        self.search_publishes = QtGui.QToolButton(Dialog)
        self.search_publishes.setMinimumSize(QtCore.QSize(0, 26))
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap(":/res/search.png"), QtGui.QIcon.Normal,
                        QtGui.QIcon.Off)
        self.search_publishes.setIcon(icon3)
        self.search_publishes.setCheckable(True)
        self.search_publishes.setObjectName("search_publishes")
        self.horizontalLayout_2.addWidget(self.search_publishes)
        self.info = QtGui.QToolButton(Dialog)
        self.info.setMinimumSize(QtCore.QSize(80, 26))
        self.info.setObjectName("info")
        self.horizontalLayout_2.addWidget(self.info)
        self.middle_area.addLayout(self.horizontalLayout_2)
        self.publish_frame = QtGui.QFrame(Dialog)
        self.publish_frame.setObjectName("publish_frame")
        self.horizontalLayout_7 = QtGui.QHBoxLayout(self.publish_frame)
        self.horizontalLayout_7.setSpacing(1)
        self.horizontalLayout_7.setContentsMargins(1, 1, 1, 1)
        self.horizontalLayout_7.setObjectName("horizontalLayout_7")
        self.publish_view = QtGui.QListView(self.publish_frame)
        self.publish_view.setEditTriggers(
            QtGui.QAbstractItemView.NoEditTriggers)
        self.publish_view.setResizeMode(QtGui.QListView.Adjust)
        self.publish_view.setSpacing(5)
        self.publish_view.setViewMode(QtGui.QListView.IconMode)
        self.publish_view.setUniformItemSizes(True)
        self.publish_view.setObjectName("publish_view")
        self.horizontalLayout_7.addWidget(self.publish_view)
        self.middle_area.addWidget(self.publish_frame)
        self.horizontalLayout_4 = QtGui.QHBoxLayout()
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")
        self.show_sub_items = QtGui.QCheckBox(Dialog)
        self.show_sub_items.setObjectName("show_sub_items")
        self.horizontalLayout_4.addWidget(self.show_sub_items)
        spacerItem1 = QtGui.QSpacerItem(128, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_4.addItem(spacerItem1)
        self.scale_label = QtGui.QLabel(Dialog)
        self.scale_label.setText("")
        self.scale_label.setPixmap(QtGui.QPixmap(":/res/search.png"))
        self.scale_label.setObjectName("scale_label")
        self.horizontalLayout_4.addWidget(self.scale_label)
        self.thumb_scale = QtGui.QSlider(Dialog)
        self.thumb_scale.setMinimumSize(QtCore.QSize(100, 0))
        self.thumb_scale.setMaximumSize(QtCore.QSize(100, 16777215))
        self.thumb_scale.setStyleSheet(
            "QSlider::groove:horizontal {\n"
            "     /*border: 1px solid #999999; */\n"
            "     height: 2px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */\n"
            "     background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3F3F3F, stop:1 #545454);\n"
            "     margin: 2px 0;\n"
            "     border-radius: 1px;\n"
            " }\n"
            "\n"
            " QSlider::handle:horizontal {\n"
            "     background: #545454;\n"
            "     border: 1px solid #B6B6B6;\n"
            "     width: 5px;\n"
            "     margin: -2px 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */\n"
            "     border-radius: 3px;\n"
            " }\n"
            "")
        self.thumb_scale.setMinimum(70)
        self.thumb_scale.setMaximum(250)
        self.thumb_scale.setProperty("value", 70)
        self.thumb_scale.setSliderPosition(70)
        self.thumb_scale.setOrientation(QtCore.Qt.Horizontal)
        self.thumb_scale.setInvertedAppearance(False)
        self.thumb_scale.setInvertedControls(False)
        self.thumb_scale.setObjectName("thumb_scale")
        self.horizontalLayout_4.addWidget(self.thumb_scale)
        self.middle_area.addLayout(self.horizontalLayout_4)
        self.horizontalLayout_3.addLayout(self.middle_area)
        self.details = QtGui.QGroupBox(Dialog)
        self.details.setMinimumSize(QtCore.QSize(300, 0))
        self.details.setMaximumSize(QtCore.QSize(300, 16777215))
        self.details.setTitle("")
        self.details.setObjectName("details")
        self.verticalLayout_3 = QtGui.QVBoxLayout(self.details)
        self.verticalLayout_3.setSpacing(2)
        self.verticalLayout_3.setContentsMargins(4, 4, 4, 4)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.horizontalLayout = QtGui.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem2)
        self.details_image = QtGui.QLabel(self.details)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.details_image.sizePolicy().hasHeightForWidth())
        self.details_image.setSizePolicy(sizePolicy)
        self.details_image.setMinimumSize(QtCore.QSize(256, 200))
        self.details_image.setMaximumSize(QtCore.QSize(256, 200))
        self.details_image.setScaledContents(True)
        self.details_image.setAlignment(QtCore.Qt.AlignCenter)
        self.details_image.setObjectName("details_image")
        self.horizontalLayout.addWidget(self.details_image)
        spacerItem3 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem3)
        self.verticalLayout_3.addLayout(self.horizontalLayout)
        self.horizontalLayout_5 = QtGui.QHBoxLayout()
        self.horizontalLayout_5.setObjectName("horizontalLayout_5")
        self.details_header = QtGui.QLabel(self.details)
        self.details_header.setAlignment(QtCore.Qt.AlignLeading
                                         | QtCore.Qt.AlignLeft
                                         | QtCore.Qt.AlignTop)
        self.details_header.setWordWrap(True)
        self.details_header.setObjectName("details_header")
        self.horizontalLayout_5.addWidget(self.details_header)
        spacerItem4 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_5.addItem(spacerItem4)
        self.verticalLayout_4 = QtGui.QVBoxLayout()
        self.verticalLayout_4.setObjectName("verticalLayout_4")
        self.detail_playback_btn = QtGui.QToolButton(self.details)
        self.detail_playback_btn.setMinimumSize(QtCore.QSize(55, 55))
        self.detail_playback_btn.setMaximumSize(QtCore.QSize(55, 55))
        self.detail_playback_btn.setText("")
        icon4 = QtGui.QIcon()
        icon4.addPixmap(QtGui.QPixmap(":/res/play_icon.png"),
                        QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.detail_playback_btn.setIcon(icon4)
        self.detail_playback_btn.setIconSize(QtCore.QSize(40, 40))
        self.detail_playback_btn.setToolButtonStyle(
            QtCore.Qt.ToolButtonTextBesideIcon)
        self.detail_playback_btn.setObjectName("detail_playback_btn")
        self.verticalLayout_4.addWidget(self.detail_playback_btn)
        self.detail_actions_btn = QtGui.QToolButton(self.details)
        self.detail_actions_btn.setMinimumSize(QtCore.QSize(55, 0))
        self.detail_actions_btn.setMaximumSize(QtCore.QSize(55, 16777215))
        self.detail_actions_btn.setPopupMode(QtGui.QToolButton.InstantPopup)
        self.detail_actions_btn.setToolButtonStyle(
            QtCore.Qt.ToolButtonTextOnly)
        self.detail_actions_btn.setObjectName("detail_actions_btn")
        self.verticalLayout_4.addWidget(self.detail_actions_btn)
        self.horizontalLayout_5.addLayout(self.verticalLayout_4)
        self.verticalLayout_3.addLayout(self.horizontalLayout_5)
        self.version_history_label = QtGui.QLabel(self.details)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.Maximum)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.version_history_label.sizePolicy().hasHeightForWidth())
        self.version_history_label.setSizePolicy(sizePolicy)
        self.version_history_label.setStyleSheet("QLabel { padding-top: 14px}")
        self.version_history_label.setAlignment(QtCore.Qt.AlignCenter)
        self.version_history_label.setWordWrap(True)
        self.version_history_label.setObjectName("version_history_label")
        self.verticalLayout_3.addWidget(self.version_history_label)
        self.history_view = QtGui.QListView(self.details)
        self.history_view.setVerticalScrollMode(
            QtGui.QAbstractItemView.ScrollPerPixel)
        self.history_view.setHorizontalScrollMode(
            QtGui.QAbstractItemView.ScrollPerPixel)
        self.history_view.setUniformItemSizes(True)
        self.history_view.setObjectName("history_view")
        self.verticalLayout_3.addWidget(self.history_view)
        self.horizontalLayout_3.addWidget(self.details)
        self.horizontalLayout_3.setStretch(0, 1)
        self.horizontalLayout_3.setStretch(1, 2)

        self.retranslateUi(Dialog)
        self.entity_preset_tabs.setCurrentIndex(-1)
        QtCore.QMetaObject.connectSlotsByName(Dialog)
        Dialog.setTabOrder(self.navigation_home, self.navigation_prev)
        Dialog.setTabOrder(self.navigation_prev, self.navigation_next)
        Dialog.setTabOrder(self.navigation_next, self.publish_type_list)
        Dialog.setTabOrder(self.publish_type_list, self.show_sub_items)
        Dialog.setTabOrder(self.show_sub_items, self.thumb_scale)
        Dialog.setTabOrder(self.thumb_scale, self.history_view)
Example #23
0
    def setupUi(self, DesktopWindow):
        DesktopWindow.setObjectName("DesktopWindow")
        DesktopWindow.resize(427, 715)
        DesktopWindow.setMouseTracking(True)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap(":/tk-desktop/default_systray_icon.png"),
                       QtGui.QIcon.Normal, QtGui.QIcon.Off)
        DesktopWindow.setWindowIcon(icon)
        DesktopWindow.setDockNestingEnabled(False)
        DesktopWindow.setUnifiedTitleAndToolBarOnMac(False)
        self.center = QtGui.QWidget(DesktopWindow)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.center.sizePolicy().hasHeightForWidth())
        self.center.setSizePolicy(sizePolicy)
        self.center.setMouseTracking(True)
        self.center.setObjectName("center")
        self.border_layout = QtGui.QVBoxLayout(self.center)
        self.border_layout.setSpacing(0)
        self.border_layout.setContentsMargins(0, 0, 0, 0)
        self.border_layout.setObjectName("border_layout")
        self.banners = QtGui.QWidget(self.center)
        self.banners.setAutoFillBackground(True)
        self.banners.setObjectName("banners")
        self.verticalLayout_4 = QtGui.QVBoxLayout(self.banners)
        self.verticalLayout_4.setSpacing(1)
        self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout_4.setObjectName("verticalLayout_4")
        self.border_layout.addWidget(self.banners)
        self.header = QtGui.QFrame(self.center)
        self.header.setFrameShape(QtGui.QFrame.NoFrame)
        self.header.setFrameShadow(QtGui.QFrame.Raised)
        self.header.setObjectName("header")
        self.horizontalLayout_2 = QtGui.QHBoxLayout(self.header)
        self.horizontalLayout_2.setSpacing(20)
        self.horizontalLayout_2.setContentsMargins(20, 0, 20, 0)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.tabs = QtGui.QHBoxLayout()
        self.tabs.setObjectName("tabs")
        self.horizontalLayout_2.addLayout(self.tabs)
        spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                       QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_2.addItem(spacerItem)
        self.border_layout.addWidget(self.header)
        self.tab_view = QtGui.QStackedWidget(self.center)
        self.tab_view.setObjectName("tab_view")
        self.apps_tab = QtGui.QStackedWidget()
        self.apps_tab.setObjectName("apps_tab")
        self.project_browser_page = QtGui.QWidget()
        self.project_browser_page.setObjectName("project_browser_page")
        self.verticalLayout = QtGui.QVBoxLayout(self.project_browser_page)
        self.verticalLayout.setSpacing(0)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.subheader = QtGui.QFrame(self.project_browser_page)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.Minimum)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.subheader.sizePolicy().hasHeightForWidth())
        self.subheader.setSizePolicy(sizePolicy)
        self.subheader.setMaximumSize(QtCore.QSize(16777215, 60))
        self.subheader.setFrameShape(QtGui.QFrame.NoFrame)
        self.subheader.setFrameShadow(QtGui.QFrame.Plain)
        self.subheader.setLineWidth(1)
        self.subheader.setMidLineWidth(0)
        self.subheader.setObjectName("subheader")
        self.horizontalLayout = QtGui.QHBoxLayout(self.subheader)
        self.horizontalLayout.setSpacing(15)
        self.horizontalLayout.setContentsMargins(20, 15, 15, 15)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.subheader_label = QtGui.QLabel(self.subheader)
        self.subheader_label.setMouseTracking(True)
        self.subheader_label.setFocusPolicy(QtCore.Qt.WheelFocus)
        self.subheader_label.setTextInteractionFlags(
            QtCore.Qt.NoTextInteraction)
        self.subheader_label.setObjectName("subheader_label")
        self.horizontalLayout.addWidget(self.subheader_label)
        spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem1)
        self.search_frame = QtGui.QFrame(self.subheader)
        self.search_frame.setFrameShape(QtGui.QFrame.StyledPanel)
        self.search_frame.setFrameShadow(QtGui.QFrame.Raised)
        self.search_frame.setProperty("collapsed", False)
        self.search_frame.setObjectName("search_frame")
        self.horizontalLayout_6 = QtGui.QHBoxLayout(self.search_frame)
        self.horizontalLayout_6.setSpacing(2)
        self.horizontalLayout_6.setContentsMargins(5, 5, 5, 5)
        self.horizontalLayout_6.setObjectName("horizontalLayout_6")
        self.search_magnifier = QtGui.QLabel(self.search_frame)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.search_magnifier.sizePolicy().hasHeightForWidth())
        self.search_magnifier.setSizePolicy(sizePolicy)
        self.search_magnifier.setMaximumSize(QtCore.QSize(17, 17))
        self.search_magnifier.setText("")
        self.search_magnifier.setPixmap(
            QtGui.QPixmap(":/tk-desktop/search_dark.png"))
        self.search_magnifier.setScaledContents(True)
        self.search_magnifier.setObjectName("search_magnifier")
        self.horizontalLayout_6.addWidget(self.search_magnifier)
        self.search_text = QtGui.QLineEdit(self.search_frame)
        self.search_text.setObjectName("search_text")
        self.horizontalLayout_6.addWidget(self.search_text)
        self.search_button = QtGui.QPushButton(self.search_frame)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.search_button.sizePolicy().hasHeightForWidth())
        self.search_button.setSizePolicy(sizePolicy)
        self.search_button.setMaximumSize(QtCore.QSize(17, 17))
        self.search_button.setFocusPolicy(QtCore.Qt.NoFocus)
        self.search_button.setText("")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap(":/tk-desktop/icon_inbox_clear.png"),
                        QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.search_button.setIcon(icon1)
        self.search_button.setIconSize(QtCore.QSize(17, 17))
        self.search_button.setFlat(True)
        self.search_button.setObjectName("search_button")
        self.horizontalLayout_6.addWidget(self.search_button)
        self.horizontalLayout.addWidget(self.search_frame)
        self.verticalLayout.addWidget(self.subheader)
        self.projects = ActionListView(self.project_browser_page)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding,
                                       QtGui.QSizePolicy.MinimumExpanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.projects.sizePolicy().hasHeightForWidth())
        self.projects.setSizePolicy(sizePolicy)
        self.projects.setMouseTracking(True)
        self.projects.setFocusPolicy(QtCore.Qt.NoFocus)
        self.projects.setFrameShape(QtGui.QFrame.NoFrame)
        self.projects.setFrameShadow(QtGui.QFrame.Plain)
        self.projects.setLineWidth(0)
        self.projects.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.projects.setAutoScroll(False)
        self.projects.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
        self.projects.setProperty("showDropIndicator", False)
        self.projects.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
        self.projects.setVerticalScrollMode(
            QtGui.QAbstractItemView.ScrollPerPixel)
        self.projects.setMovement(QtGui.QListView.Static)
        self.projects.setFlow(QtGui.QListView.LeftToRight)
        self.projects.setProperty("isWrapping", True)
        self.projects.setResizeMode(QtGui.QListView.Adjust)
        self.projects.setLayoutMode(QtGui.QListView.SinglePass)
        self.projects.setSpacing(5)
        self.projects.setViewMode(QtGui.QListView.IconMode)
        self.projects.setUniformItemSizes(False)
        self.projects.setSelectionRectVisible(False)
        self.projects.setObjectName("projects")
        self.verticalLayout.addWidget(self.projects)
        self.apps_tab.addWidget(self.project_browser_page)
        self.project_page = QtGui.QWidget()
        self.project_page.setObjectName("project_page")
        self.verticalLayout_2 = QtGui.QVBoxLayout(self.project_page)
        self.verticalLayout_2.setSpacing(0)
        self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.project_subheader = QtGui.QFrame(self.project_page)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.project_subheader.sizePolicy().hasHeightForWidth())
        self.project_subheader.setSizePolicy(sizePolicy)
        self.project_subheader.setMaximumSize(QtCore.QSize(16777215, 60))
        self.project_subheader.setFrameShape(QtGui.QFrame.NoFrame)
        self.project_subheader.setFrameShadow(QtGui.QFrame.Plain)
        self.project_subheader.setLineWidth(1)
        self.project_subheader.setMidLineWidth(0)
        self.project_subheader.setObjectName("project_subheader")
        self.horizontalLayout_4 = QtGui.QHBoxLayout(self.project_subheader)
        self.horizontalLayout_4.setSpacing(0)
        self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")
        self.spacer_button_1 = QtGui.QPushButton(self.project_subheader)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.spacer_button_1.sizePolicy().hasHeightForWidth())
        self.spacer_button_1.setSizePolicy(sizePolicy)
        self.spacer_button_1.setMinimumSize(QtCore.QSize(10, 0))
        self.spacer_button_1.setMaximumSize(QtCore.QSize(10, 16777215))
        self.spacer_button_1.setBaseSize(QtCore.QSize(10, 0))
        self.spacer_button_1.setFocusPolicy(QtCore.Qt.NoFocus)
        self.spacer_button_1.setText("")
        self.spacer_button_1.setFlat(True)
        self.spacer_button_1.setObjectName("spacer_button_1")
        self.horizontalLayout_4.addWidget(self.spacer_button_1)
        self.project_arrow = QtGui.QPushButton(self.project_subheader)
        self.project_arrow.setMaximumSize(QtCore.QSize(30, 62))
        self.project_arrow.setFocusPolicy(QtCore.Qt.NoFocus)
        self.project_arrow.setText("")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap(":/tk-desktop/back_arrow.png"),
                        QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.project_arrow.setIcon(icon2)
        self.project_arrow.setIconSize(QtCore.QSize(20, 20))
        self.project_arrow.setFlat(True)
        self.project_arrow.setObjectName("project_arrow")
        self.horizontalLayout_4.addWidget(self.project_arrow)
        spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_4.addItem(spacerItem2)
        self.project_icon = QtGui.QLabel(self.project_subheader)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
                                       QtGui.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.project_icon.sizePolicy().hasHeightForWidth())
        self.project_icon.setSizePolicy(sizePolicy)
        self.project_icon.setMaximumSize(QtCore.QSize(42, 42))
        self.project_icon.setText("")
        self.project_icon.setPixmap(
            QtGui.QPixmap(":/tk-desktop/missing_thumbnail_project.png"))
        self.project_icon.setScaledContents(True)
        self.project_icon.setMargin(5)
        self.project_icon.setObjectName("project_icon")
        self.horizontalLayout_4.addWidget(self.project_icon)
        self.project_name = QtGui.QLabel(self.project_subheader)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
                                       QtGui.QSizePolicy.MinimumExpanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.project_name.sizePolicy().hasHeightForWidth())
        self.project_name.setSizePolicy(sizePolicy)
        self.project_name.setMaximumSize(QtCore.QSize(280, 16777215))
        self.project_name.setMargin(5)
        self.project_name.setObjectName("project_name")
        self.horizontalLayout_4.addWidget(self.project_name)
        spacerItem3 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_4.addItem(spacerItem3)
        self.project_menu = QtGui.QToolButton(self.project_subheader)
        self.project_menu.setFocusPolicy(QtCore.Qt.NoFocus)
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap(":/tk-desktop/down_arrow.png"),
                        QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.project_menu.setIcon(icon3)
        self.project_menu.setIconSize(QtCore.QSize(20, 20))
        self.project_menu.setPopupMode(QtGui.QToolButton.InstantPopup)
        self.project_menu.setObjectName("project_menu")
        self.horizontalLayout_4.addWidget(self.project_menu)
        self.spacer_button_4 = QtGui.QPushButton(self.project_subheader)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.spacer_button_4.sizePolicy().hasHeightForWidth())
        self.spacer_button_4.setSizePolicy(sizePolicy)
        self.spacer_button_4.setMinimumSize(QtCore.QSize(10, 0))
        self.spacer_button_4.setMaximumSize(QtCore.QSize(10, 16777215))
        self.spacer_button_4.setBaseSize(QtCore.QSize(10, 0))
        self.spacer_button_4.setFocusPolicy(QtCore.Qt.NoFocus)
        self.spacer_button_4.setText("")
        self.spacer_button_4.setFlat(True)
        self.spacer_button_4.setObjectName("spacer_button_4")
        self.horizontalLayout_4.addWidget(self.spacer_button_4)
        self.verticalLayout_2.addWidget(self.project_subheader)
        self.configuration_frame = QtGui.QFrame(self.project_page)
        self.configuration_frame.setFrameShape(QtGui.QFrame.NoFrame)
        self.configuration_frame.setFrameShadow(QtGui.QFrame.Plain)
        self.configuration_frame.setObjectName("configuration_frame")
        self.horizontalLayout_8 = QtGui.QHBoxLayout(self.configuration_frame)
        self.horizontalLayout_8.setSpacing(0)
        self.horizontalLayout_8.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_8.setObjectName("horizontalLayout_8")
        spacerItem4 = QtGui.QSpacerItem(150, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_8.addItem(spacerItem4)
        self.configuration_name = QtGui.QLabel(self.configuration_frame)
        self.configuration_name.setAlignment(QtCore.Qt.AlignCenter)
        self.configuration_name.setObjectName("configuration_name")
        self.horizontalLayout_8.addWidget(self.configuration_name)
        self.configuration_label = QtGui.QLabel(self.configuration_frame)
        self.configuration_label.setAlignment(QtCore.Qt.AlignRight
                                              | QtCore.Qt.AlignTrailing
                                              | QtCore.Qt.AlignVCenter)
        self.configuration_label.setObjectName("configuration_label")
        self.horizontalLayout_8.addWidget(self.configuration_label)
        self.horizontalLayout_8.setStretch(0, 1)
        self.horizontalLayout_8.setStretch(1, 1)
        self.horizontalLayout_8.setStretch(2, 1)
        self.verticalLayout_2.addWidget(self.configuration_frame)
        self.command_panel_area = QtGui.QScrollArea(self.project_page)
        self.command_panel_area.setStyleSheet("QScrollArea {\n"
                                              "border: 0, 0, 0, 0\n"
                                              "}")
        self.command_panel_area.setWidgetResizable(True)
        self.command_panel_area.setAlignment(QtCore.Qt.AlignLeading
                                             | QtCore.Qt.AlignLeft
                                             | QtCore.Qt.AlignTop)
        self.command_panel_area.setObjectName("command_panel_area")
        self.scrollAreaWidgetContents_3 = QtGui.QWidget()
        self.scrollAreaWidgetContents_3.setGeometry(QtCore.QRect(
            0, 0, 100, 30))
        self.scrollAreaWidgetContents_3.setObjectName(
            "scrollAreaWidgetContents_3")
        self.command_panel_area.setWidget(self.scrollAreaWidgetContents_3)
        self.verticalLayout_2.addWidget(self.command_panel_area)
        self.apps_tab.addWidget(self.project_page)
        self.tab_view.addWidget(self.apps_tab)
        self.border_layout.addWidget(self.tab_view)
        self.footer = QtGui.QFrame(self.center)
        self.footer.setMouseTracking(True)
        self.footer.setFrameShape(QtGui.QFrame.NoFrame)
        self.footer.setFrameShadow(QtGui.QFrame.Plain)
        self.footer.setObjectName("footer")
        self.horizontalLayout_3 = QtGui.QHBoxLayout(self.footer)
        self.horizontalLayout_3.setSpacing(0)
        self.horizontalLayout_3.setContentsMargins(10, 5, 10, 5)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.shotgun_button = QtGui.QPushButton(self.footer)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
                                       QtGui.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.shotgun_button.sizePolicy().hasHeightForWidth())
        self.shotgun_button.setSizePolicy(sizePolicy)
        self.shotgun_button.setMinimumSize(QtCore.QSize(132, 26))
        self.shotgun_button.setMaximumSize(QtCore.QSize(132, 26))
        self.shotgun_button.setMouseTracking(True)
        self.shotgun_button.setFocusPolicy(QtCore.Qt.NoFocus)
        icon4 = QtGui.QIcon()
        icon4.addPixmap(
            QtGui.QPixmap(":/tk-desktop/shotgun_logo_light_medium.png"),
            QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.shotgun_button.setIcon(icon4)
        self.shotgun_button.setIconSize(QtCore.QSize(122, 16))
        self.shotgun_button.setFlat(True)
        self.shotgun_button.setObjectName("shotgun_button")
        self.horizontalLayout_3.addWidget(self.shotgun_button)
        spacerItem5 = QtGui.QSpacerItem(173, 20, QtGui.QSizePolicy.Expanding,
                                        QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_3.addItem(spacerItem5)
        self.user_button = QtGui.QPushButton(self.footer)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.user_button.sizePolicy().hasHeightForWidth())
        self.user_button.setSizePolicy(sizePolicy)
        self.user_button.setMinimumSize(QtCore.QSize(40, 40))
        self.user_button.setMaximumSize(QtCore.QSize(40, 40))
        self.user_button.setMouseTracking(True)
        self.user_button.setFocusPolicy(QtCore.Qt.NoFocus)
        self.user_button.setText("")
        icon5 = QtGui.QIcon()
        icon5.addPixmap(QtGui.QPixmap(":/tk-desktop/default_user_thumb.png"),
                        QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.user_button.setIcon(icon5)
        self.user_button.setIconSize(QtCore.QSize(30, 30))
        self.user_button.setFlat(True)
        self.user_button.setObjectName("user_button")
        self.horizontalLayout_3.addWidget(self.user_button)
        self.border_layout.addWidget(self.footer)
        DesktopWindow.setCentralWidget(self.center)
        self.actionQuit = QtGui.QAction(DesktopWindow)
        self.actionQuit.setObjectName("actionQuit")
        self.actionPin_to_Menu = QtGui.QAction(DesktopWindow)
        self.actionPin_to_Menu.setObjectName("actionPin_to_Menu")
        self.actionSign_Out = QtGui.QAction(DesktopWindow)
        self.actionSign_Out.setObjectName("actionSign_Out")
        self.actionKeep_on_Top = QtGui.QAction(DesktopWindow)
        self.actionKeep_on_Top.setCheckable(True)
        self.actionKeep_on_Top.setObjectName("actionKeep_on_Top")
        self.actionProject_Filesystem_Folder = QtGui.QAction(DesktopWindow)
        self.actionProject_Filesystem_Folder.setObjectName(
            "actionProject_Filesystem_Folder")
        self.actionShow_Console = QtGui.QAction(DesktopWindow)
        self.actionShow_Console.setObjectName("actionShow_Console")
        self.actionRefresh_Projects = QtGui.QAction(DesktopWindow)
        self.actionRefresh_Projects.setObjectName("actionRefresh_Projects")
        self.actionAdvanced_Project_Setup = QtGui.QAction(DesktopWindow)
        self.actionAdvanced_Project_Setup.setObjectName(
            "actionAdvanced_Project_Setup")
        self.actionHelp = QtGui.QAction(DesktopWindow)
        self.actionHelp.setObjectName("actionHelp")
        self.actionRegenerate_Certificates = QtGui.QAction(DesktopWindow)
        self.actionRegenerate_Certificates.setObjectName(
            "actionRegenerate_Certificates")

        self.retranslateUi(DesktopWindow)
        self.apps_tab.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(DesktopWindow)
        DesktopWindow.setTabOrder(self.projects, self.user_button)
        DesktopWindow.setTabOrder(self.user_button, self.search_button)
        DesktopWindow.setTabOrder(self.search_button, self.search_text)
Example #24
0
class ReplyListWidget(QtGui.QWidget):
    """
    QT Widget that displays a note conversation, 
    including attachments and the ability to reply.

    This will first render the body of the note, including the attachments,
    and then subsequent replies. This widget uses the same
    widgets, data backend and visual components as the 
    activity stream.
    
    :signal entity_requested(str, int): Fires when someone clicks an entity inside
            the activity stream. The returned parameters are entity type and entity id.
    """

    # when someone clicks a link or similar
    entity_requested = QtCore.Signal(str, int)

    def __init__(self, parent):
        """
        :param parent: QT parent object
        :type parent: :class:`~PySide.QtGui.QWidget`
        """

        # first, call the base class and let it do its thing.
        QtGui.QWidget.__init__(self, parent)

        # now load in the UI that was created in the UI designer
        self.ui = Ui_ReplyListWidget()
        self.ui.setupUi(self)

        self._note_id = None
        self._sg_entity_dict = None
        self._task_manager = None
        self._general_widgets = []
        self._reply_widgets = []
        self._attachment_group_widgets = {}

        self._bundle = sgtk.platform.current_bundle()

        # apply styling
        self._load_stylesheet()

        # small overlay
        self.__small_overlay = SmallOverlayWidget(self)

        # create a data manager to handle backend
        self._data_manager = ActivityStreamDataHandler(self)

        self._data_manager.thumbnail_arrived.connect(self._process_thumbnail)
        self._data_manager.note_arrived.connect(self._process_note)

    def set_bg_task_manager(self, task_manager):
        """
        Specify the background task manager to use to pull
        data in the background. Data calls
        to Shotgun will be dispatched via this object.
        
        :param task_manager: Background task manager to use
        :type task_manager: :class:`~tk-framework-shotgunutils:task_manager.BackgroundTaskManager` 
        """
        self._data_manager.set_bg_task_manager(task_manager)
        self._task_manager = task_manager

    def destroy(self):
        """
        Should be called before the widget is closed
        """
        self._data_manager.destroy()
        self._task_manager = None

    ##########################################################################################
    # public interface

    def load_data(self, sg_entity_dict):
        """
        Load replies for a given entity.
        
        :param sg_entity_dict: Shotgun link dictionary with keys type and id.
        """
        self._bundle.log_debug("Loading replies for %s" % sg_entity_dict)

        if sg_entity_dict["type"] != "Note":
            self._bundle.log_error("Can only show replies for Notes.")
            return

        # first ask the data manager to load up cached
        # information about our note
        self._sg_entity_dict = sg_entity_dict
        note_id = self._sg_entity_dict["id"]
        self._data_manager.load_note_data(note_id)

        # now attempt to render the note based on cached data
        self._process_note(activity_id=None, note_id=note_id)

        # and read in any updates in the background
        self._data_manager.rescan()

    ##########################################################################################
    # internal methods

    def _process_note(self, activity_id, note_id):
        """
        Callback that gets executed when note data arrives from
        the data manager.
        
        :param activiy_id: Activity stream id that this note is 
                           associated with. Note that in this case,
                           when we have requested a note outside
                           the context of the activity stream, this
                           value is undefined.
        :param note_id: Note id for the note for which data is available
                        in the data manager.
        """

        self._bundle.log_debug("Retrieved new data notification for "
                               "activity id %s, note id %s" %
                               (activity_id, note_id))

        # set note content
        note_thread_data = self._data_manager.get_note(note_id)

        if note_thread_data:
            self._build_replies(note_thread_data)

    def _build_replies(self, note_thread_data):

        # before we begin widget operations, turn off visibility
        # of the whole widget in order to avoid recomputes
        self.setVisible(False)

        try:

            ###############################################################
            # Phase 1 - render the UI.

            self._clear()

            note_id = self._sg_entity_dict["id"]
            attachment_requests = []

            # first display the content of the note
            note_data = note_thread_data[0]

            note_content = note_data.get("content") or \
                           "This note does not have any content associated."

            content_widget = QtGui.QLabel(self)
            content_widget.setWordWrap(True)
            content_widget.setText(note_content)
            content_widget.setObjectName("note_content_label")
            self.ui.reply_layout.addWidget(content_widget)
            self._general_widgets.append(content_widget)

            # we have cached note data
            replies_and_attachments = note_thread_data[1:]

            # now add replies
            self._add_replies_and_attachments(replies_and_attachments)

            # add a reply button and connect it
            reply_button = self._add_reply_button()
            reply_button.clicked.connect(
                lambda: self._on_reply_clicked(note_id))

            # add a proxy widget that should expand to fill all white
            # space available
            expanding_widget = QtGui.QLabel(self)
            self.ui.reply_layout.addWidget(expanding_widget)
            self.ui.reply_layout.setStretchFactor(expanding_widget, 1)
            self._general_widgets.append(expanding_widget)

            ###############################################################
            # Phase 2 - request additional data.
            # note that we don't interleave these requests with building
            # the ui - this is to minimise the risk of GIL signal issues

            # get all attachment data
            # can request thumbnails post UI build
            for attachment_group_id in self._attachment_group_widgets.keys():
                agw = self._attachment_group_widgets[attachment_group_id]
                for attachment_data in agw.get_data():
                    ag_request = {
                        "attachment_group_id": attachment_group_id,
                        "attachment_data": attachment_data
                    }
                    attachment_requests.append(ag_request)

            self._bundle.log_debug("Request thumbnails...")

            for attachment_req in attachment_requests:
                self._data_manager.request_attachment_thumbnail(
                    -1, attachment_req["attachment_group_id"],
                    attachment_req["attachment_data"])

            # now go through the shotgun data
            # for each reply, request a thumbnail.
            requested_items = []
            for item in replies_and_attachments:
                if item["type"] == "Reply":
                    # note that the reply data structure is special:
                    # the 'user' key is not a normal sg link dict,
                    # but contains an additional image field to describe
                    # the thumbnail:
                    #
                    # {'content': 'Reply content...',
                    #  'created_at': 1438649419.0,
                    #  'type': 'Reply',
                    #  'id': 73,
                    #  'user': {'image': '...',
                    #           'type': 'HumanUser',
                    #           'id': 38,
                    #           'name': 'Manne Ohrstrom'}}]
                    reply_author = item["user"]

                    uniqueness_key = (reply_author["type"], reply_author["id"])
                    if uniqueness_key not in requested_items:
                        # this thumbnail has not been requested yet
                        if reply_author.get("image"):
                            # there is a thumbnail for this user!
                            requested_items.append(uniqueness_key)
                            self._data_manager.request_user_thumbnail(
                                reply_author["type"], reply_author["id"],
                                reply_author["image"])

        finally:
            # make the window visible again and trigger a redraw
            self.setVisible(True)

        self._bundle.log_debug("...done")

    def _clear(self):
        """
        Clear the widget. This will remove all items from the UI
        """
        self._bundle.log_debug("Clearing UI...")

        for x in self._general_widgets + self._reply_widgets + self._attachment_group_widgets.values(
        ):
            # remove widget from layout:
            self.ui.reply_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._general_widgets = []
        self._reply_widgets = []
        self._attachment_group_widgets = {}

    def _load_stylesheet(self):
        """
        Loads in a stylesheet from disk
        """
        qss_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                "style.qss")
        try:
            f = open(qss_file, "rt")
            qss_data = f.read()
            # apply to widget (and all its children)
            self.setStyleSheet(qss_data)
        finally:
            f.close()

    def _add_reply_button(self):
        """
        Add a reply button to the stream of widgets
        """
        reply_button = ClickableLabel(self)
        reply_button.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTop)
        reply_button.setText("Reply to this Note")
        reply_button.setObjectName("reply_button")
        self.ui.reply_layout.addWidget(reply_button)
        self._general_widgets.append(reply_button)
        return reply_button

    def _add_attachment_group(self, attachments, after_note):
        """
        Add an attachments group to the stream of widgets
        """

        curr_attachment_group_widget_id = len(self._attachment_group_widgets)
        attachment_group = AttachmentGroupWidget(self, attachments)

        # show an 'ATTACHMENTS' header
        attachment_group.show_attachments_label(True)

        offset = attachment_group.OFFSET_NONE if after_note else attachment_group.OFFSET_LARGE_THUMB
        attachment_group.adjust_left_offset(offset)

        self.ui.reply_layout.addWidget(attachment_group)

        # add it to our mapping dict and increment the counter
        self._attachment_group_widgets[
            curr_attachment_group_widget_id] = attachment_group

    def _add_replies_and_attachments(self, replies_and_attachments):
        """
        Add replies and attachment widgets to the stream of widgets
        
        :param replies_and_attachments: List of Shotgun data dictionary.
               These are eithere reply entities or attachment entities.
        """

        current_attachments = []
        attachment_is_directly_after_note = True

        for item in replies_and_attachments:

            if item["type"] == "Reply":

                # first, wrap up attachments
                if len(current_attachments) > 0:
                    self._add_attachment_group(
                        current_attachments, attachment_is_directly_after_note)
                    current_attachments = []

                w = ReplyWidget(self)
                w.adjust_thumb_style(w.LARGE_USER_THUMB)
                self.ui.reply_layout.addWidget(w)
                w.set_info(item)
                self._reply_widgets.append(w)
                # ensure navigation requests from replies bubble up
                w.entity_requested.connect(self.entity_requested.emit)
                # next bunch of attachments will be after a reply
                # rather than directly under the note
                # (this affects the visual style)
                attachment_is_directly_after_note = False

            if item["type"] == "Attachment" and item["this_file"][
                    "link_type"] == "upload":
                current_attachments.append(item)

        # see if there are still open attachments
        if len(current_attachments) > 0:
            self._add_attachment_group(current_attachments,
                                       attachment_is_directly_after_note)
            current_attachments = []

    def _process_thumbnail(self, data):
        """
        Callback that gets called when a new thumbnail is available.
        Populate the UI with the given thumbnail
        
        :param data: dictionary with keys:
                     - thumbnail_type: thumbnail enum constant:
                            ActivityStreamDataHandler.THUMBNAIL_CREATED_BY
                            ActivityStreamDataHandler.THUMBNAIL_ENTITY
                            ActivityStreamDataHandler.THUMBNAIL_ATTACHMENT
                     - activity_id: Activity stream id that this update relates
                       to. Note requests (which don't have an associated 
                       id, will use -1 to indicate this). 
                     
        
        QImage with thumbnail data
        :param thumbnail_type: thumbnail enum constant:
        """
        thumbnail_type = data["thumbnail_type"]
        activity_id = data["activity_id"]
        image = data["image"]

        if thumbnail_type == ActivityStreamDataHandler.THUMBNAIL_ATTACHMENT and activity_id == -1:
            group_id = data["attachment_group_id"]
            attachment_group = self._attachment_group_widgets[group_id]
            attachment_group.apply_thumbnail(data)

        elif thumbnail_type == ActivityStreamDataHandler.THUMBNAIL_USER:
            # a thumbnail for a user possibly for one of our replies
            for reply_widget in self._reply_widgets:
                if reply_widget.thumbnail_populated:
                    # already set
                    continue
                if data["entity"] == reply_widget.created_by:
                    reply_widget.set_thumbnail(image)

    def _on_reply_clicked(self, note_id):
        """
        Callback when someone clicks reply to note
        
        :param note_id: Note id to reply to
        """

        # TODO - refactor to avoid having this code in two places

        # create reply dialog window
        reply_dialog = ReplyDialog(self, self._task_manager, note_id)

        # position the reply modal dialog above the activity stream scroll area
        pos = self.mapToGlobal(self.ui.reply_scroll_area.pos())
        x_pos = pos.x() + (self.ui.reply_scroll_area.width() /
                           2) - (reply_dialog.width() / 2) - 10
        y_pos = pos.y() + (self.ui.reply_scroll_area.height() /
                           2) - (reply_dialog.height() / 2) - 20
        reply_dialog.move(x_pos, y_pos)

        # show the dialog, and while it's showing,
        # enable a transparent overlay on top of the existing replies
        # in order to make the reply window stand out.
        try:
            self.__small_overlay.show()
            if reply_dialog.exec_() == QtGui.QDialog.Accepted:
                self._data_manager.rescan()
        finally:
            self.__small_overlay.hide()
class AsyncFileFinder(FileFinder):
    """
    Version of the file finder that can perform multiple searches asyncrounously emitting
    any files found via signals as they are found.
    """
    class _SearchData(object):
        def __init__(self, search_id, entity, users, publish_model):
            """
            """
            self.id = search_id
            self.entity = copy.deepcopy(entity)
            self.users = copy.deepcopy(users)
            self.publish_model = publish_model
            self.publish_model_refreshed = False
            self.aborted = False

            self.name_map = FileFinder._FileNameMap()

            self.construct_work_area_task = None
            self.resolve_work_area_task = None
            self.find_work_files_tasks = set()
            self.load_cached_pubs_task = None
            self.find_publishes_tasks = set()
            self.user_work_areas = {}

    _FIND_PUBLISHES_PRIORITY, _FIND_FILES_PRIORITY = (20, 40)

    # Signals
    work_area_found = QtCore.Signal(object, object)
    work_area_resolved = QtCore.Signal(object, object)  # search_id, WorkArea
    files_found = QtCore.Signal(object, object,
                                object)  # search_id, file list, WorkArea
    publishes_found = QtCore.Signal(object, object,
                                    object)  # search_id, file list, WorkArea
    search_failed = QtCore.Signal(object, object)  # search_id, message
    search_completed = QtCore.Signal(object)  # search_id

    def __init__(self, bg_task_manager, parent=None):
        """
        """
        FileFinder.__init__(self, parent)

        self._searches = {}
        self._available_publish_models = []

        self._bg_task_manager = bg_task_manager
        self._bg_task_manager.task_completed.connect(
            self._on_background_task_completed)
        self._bg_task_manager.task_failed.connect(
            self._on_background_task_failed)
        self._bg_task_manager.task_group_finished.connect(
            self._on_background_search_finished)

    def shut_down(self):
        """
        """
        # clean up any publish models - not doing this will result in
        # severe instability!
        for search in self._searches:
            if search.publish_model:
                self._available_publish_models.append(search.publish_model)
        self._searches = {}
        for publish_model in self._available_publish_models:
            publish_model.destroy()
        self._available_publish_models = []

        # and shut down the task manager
        if self._bg_task_manager:
            # disconnect from the task manager:
            self._bg_task_manager.task_completed.disconnect(
                self._on_background_task_completed)
            self._bg_task_manager.task_failed.disconnect(
                self._on_background_task_failed)
            self._bg_task_manager.task_group_finished.disconnect(
                self._on_background_search_finished)

    def begin_search(self, entity, users=None):
        """
        A full search involves several stages:

        Stage 1;
           - Construct Work Area
               - resolve user sandboxes

        Stage 2:
           - find work-files
               - process work files
           - find cached publishes
               - process cached publishes
           - refresh un-cached publishes

        Stage 3:
           - Process un-cached publishes

        :param entity:  The entity to search for files for
        :param users:   A list of user sandboxes to search for files for.  If 'None' then only files for the current
                        users sandbox will be searched for.
        """
        users = users or []

        # get a new unique group id from the task manager - this will be used as the search id
        search_id = self._bg_task_manager.next_group_id()

        # get a publish model - re-use if possible, otherwise create a new one.  Max number of publish
        # models created will be the max number of searches at any one time.
        publish_model = None
        if self._available_publish_models:
            # re-use an existing publish model:
            publish_model = self._available_publish_models.pop(0)
        else:
            # create a new model:
            publish_model = SgPublishedFilesModel(search_id,
                                                  self._bg_task_manager,
                                                  parent=self)
            publish_model.data_refreshed.connect(
                self._on_publish_model_refreshed)
            publish_model.data_refresh_fail.connect(
                self._on_publish_model_refresh_failed)
            monitor_qobject_lifetime(publish_model, "Finder publish model")
        publish_model.uid = search_id

        # construct the new search data:
        search = AsyncFileFinder._SearchData(search_id, entity, users,
                                             publish_model)
        self._searches[search.id] = search

        # begin the search stage 1:
        self._begin_search_stage_1(search)

        # and return the search id:
        return search.id

    def _begin_search_stage_1(self, search):
        """
        """
        # start Stage 1 to construct the work area:
        # 1a. Construct a work area for the entity.  The work area contains the context as well as
        # all settings, etc. specific to the work area.
        search.construct_work_area_task = self._bg_task_manager.add_task(
            self._task_construct_work_area,
            group=search.id,
            task_kwargs={"entity": search.entity})

        # 1b. Resolve sandbox users for the work area (if there are any)
        search.resolve_work_area_task = self._bg_task_manager.add_task(
            self._task_resolve_sandbox_users,
            group=search.id,
            upstream_task_ids=[search.construct_work_area_task])

    def _begin_search_for_work_files(self, search, work_area):
        """
        """

        # 2a. Add tasks to find and filter work files:
        for user in search.users:
            user_id = user["id"] if user else None

            # create a copy of the work area for this user:
            user_work_area = work_area.create_copy_for_user(
                user) if user else work_area
            search.user_work_areas[user_id] = user_work_area

            # find work files:
            find_work_files_task = self._bg_task_manager.add_task(
                self._task_find_work_files,
                group=search.id,
                priority=AsyncFileFinder._FIND_FILES_PRIORITY,
                task_kwargs={"environment": user_work_area})

            # filter work files:
            filter_work_files_task = self._bg_task_manager.add_task(
                self._task_filter_work_files,
                group=search.id,
                priority=AsyncFileFinder._FIND_FILES_PRIORITY,
                upstream_task_ids=[find_work_files_task],
                task_kwargs={"environment": user_work_area})

            # build work items:
            process_work_items_task = self._bg_task_manager.add_task(
                self._task_process_work_items,
                group=search.id,
                priority=AsyncFileFinder._FIND_FILES_PRIORITY,
                upstream_task_ids=[filter_work_files_task],
                task_kwargs={
                    "environment": user_work_area,
                    "name_map": search.name_map
                })
            search.find_work_files_tasks.add(process_work_items_task)

    def _begin_search_process_publishes(self, search, sg_publishes):
        """
        """
        # 3a. Process publishes
        for user in search.users:
            user_id = user["id"] if user else None
            user_work_area = search.user_work_areas[user_id]

            users_publishes = copy.deepcopy(sg_publishes)

            # filter publishes:
            filter_publishes_task = self._bg_task_manager.add_task(
                self._task_filter_publishes,
                group=search.id,
                priority=AsyncFileFinder._FIND_PUBLISHES_PRIORITY,
                task_kwargs={
                    "environment": user_work_area,
                    "sg_publishes": users_publishes
                })
            # build publish items:
            process_publish_items_task = self._bg_task_manager.add_task(
                self._task_process_publish_items,
                group=search.id,
                priority=AsyncFileFinder._FIND_PUBLISHES_PRIORITY,
                upstream_task_ids=[filter_publishes_task],
                task_kwargs={
                    "environment": user_work_area,
                    "name_map": search.name_map
                })

            search.find_publishes_tasks.add(process_publish_items_task)

    def _on_publish_model_refreshed(self, data_changed):
        """
        """
        model = self.sender()
        if model.uid not in self._searches:
            return
        search = self._searches[model.uid]
        search.publish_model_refreshed = True

        # get any publishes from the publish model:
        sg_publishes = copy.deepcopy(search.publish_model.get_sg_data())

        # and begin processing:
        self._begin_search_process_publishes(search, sg_publishes)

    def _on_publish_model_refresh_failed(self, msg):
        """
        """
        model = self.sender()
        search_id = model.search_id
        if search_id not in self._searches:
            return
        self.stop_search(search_id)
        self.search_failed.emit(search_id, msg)

    def _on_background_task_completed(self, task_id, search_id, result):
        """
        Runs in main thread
        """
        if search_id not in self._searches:
            return
        search = self._searches[search_id]
        work_area = result.get("environment")

        if task_id == search.construct_work_area_task:
            search.construct_work_area_task = None
            self.work_area_found.emit(search_id, work_area)

            # If one or more template hasn't been configured, derail the whole process
            # and return nothing found.
            missing_templates = work_area.get_missing_templates()
            if missing_templates:
                # Notify that no files were found so the UI can update
                self.publishes_found.emit(search_id, [], work_area)
                self.files_found.emit(search_id, [], work_area)
                search.aborted = True
                return

            # we have successfully constructed a work area that we can
            # use for the next stage so begin searching for work files:
            self._begin_search_for_work_files(search, work_area)
            # and also add a task to process cached publishes:
            search.load_cached_pubs_task = self._bg_task_manager.add_pass_through_task(
                group=search.id,
                priority=AsyncFileFinder._FIND_PUBLISHES_PRIORITY,
                task_kwargs={"environment": work_area})
        elif task_id == search.resolve_work_area_task:
            search.resolve_work_area_task = None
            # found a work area so emit it:
            self.work_area_resolved.emit(search_id, work_area)

        elif task_id == search.load_cached_pubs_task:
            search.load_cached_pubs_task = None
            # ok so now it's time to load the cached publishes:
            sg_publishes = self._load_cached_publishes(search, work_area)
            # begin stage 3 for the un-cached publishes:
            self._begin_search_process_publishes(search, sg_publishes)
            # we can also start the background refresh of the publishes model:
            search.publish_model.refresh()

        elif task_id in search.find_publishes_tasks:
            search.find_publishes_tasks.remove(task_id)
            # found publishes:
            publish_item_args = result.get("publish_items", {}).values()
            files = [FileItem(**kwargs) for kwargs in publish_item_args]
            self.publishes_found.emit(search_id, files, work_area)

        elif task_id in search.find_work_files_tasks:
            search.find_work_files_tasks.remove(task_id)
            # found work files:
            work_item_args = result.get("work_items", {}).values()
            files = [FileItem(**kwargs) for kwargs in work_item_args]
            self.files_found.emit(search_id, files, work_area)

    def _on_background_task_failed(self, task_id, search_id, msg, stack_trace):
        """
        """
        if search_id not in self._searches:
            return
        self.stop_search(search_id)

        app = sgtk.platform.current_bundle()
        app.log_error(msg)
        app.log_debug(stack_trace)

        # emit signal:
        self.search_failed.emit(search_id, msg)

    def _on_background_search_finished(self, search_id):
        """
        """
        if search_id not in self._searches:
            return
        search = self._searches[search_id]

        # because of the way the search is split into stages, this signal may
        # be emitted multiple times for a single search so we need to check
        # that the search has actually finished!
        if search.users and not search.aborted:
            if (search.find_publishes_tasks or search.find_work_files_tasks
                    or search.load_cached_pubs_task
                    or not search.publish_model_refreshed):
                # we still have work outstanding!
                return

        # ok, looks like the search is actually complete!
        self.stop_search(search_id)

        # emit search completed signal:
        self.search_completed.emit(search_id)

    def stop_search(self, search_id):
        """
        """
        search = self._searches.get(search_id)
        if not search:
            return

        self._bg_task_manager.stop_task_group(search_id)
        if search.publish_model:
            search.publish_model.clear()
            self._available_publish_models.append(search.publish_model)
        del self._searches[search_id]

    def stop_all_searches(self):
        """
        """
        for search in self._searches.values():
            self._bg_task_manager.stop_task_group(search.id)
            if search.publish_model:
                self._available_publish_models.append(search.publish_model)
        self._searches = {}

    ################################################################################################
    ################################################################################################
    def _task_construct_work_area(self, entity, **kwargs):
        """
        """
        app = sgtk.platform.current_bundle()
        work_area = None
        if entity:
            # build a context from the search details:
            context = app.sgtk.context_from_entity_dictionary(entity)

            # build the work area for this context: This may throw, but the background task manager framework
            # will catch
            work_area = WorkArea(context)

        return {"environment": work_area}

    def _task_resolve_sandbox_users(self, environment, **kwargs):
        """
        """
        if environment:
            environment.resolve_user_sandboxes()
        return {"environment": environment}

    def _load_cached_publishes(self, search, work_area):
        """
        Runs in main thread.
        """
        publish_filters = []
        if work_area.context.entity:
            publish_filters.append(["entity", "is", work_area.context.entity])
        if work_area.context.task:
            publish_filters.append(["task", "is", work_area.context.task])
        elif work_area.context.step:
            publish_filters.append(
                ["task.Task.step", "is", work_area.context.step])
        fields = [
            "id", "description", "version_number", "image", "created_at",
            "created_by", "name", "path", "task"
        ]

        # load the data into the publish model:
        search.publish_model.load_data(filters=publish_filters, fields=fields)
        return copy.deepcopy(search.publish_model.get_sg_data())

    def _task_filter_publishes(self, sg_publishes, environment, **kwargs):
        """
        """
        #time.sleep(5)
        filtered_publishes = []
        if sg_publishes and environment and environment.publish_template and environment.context:

            # convert created_at unix time stamp to shotgun std time stamp for all publishes
            for sg_publish in sg_publishes:
                created_at = sg_publish.get("created_at")
                if created_at:
                    created_at = datetime.fromtimestamp(
                        created_at, sg_timezone.LocalTimezone())
                    sg_publish["created_at"] = created_at

            filtered_publishes = self._filter_publishes(
                sg_publishes, environment.publish_template,
                environment.valid_file_extensions)
        return {"sg_publishes": filtered_publishes}

    def _task_process_publish_items(self, sg_publishes, environment, name_map,
                                    **kwargs):
        """
        """
        publish_items = {}
        if (sg_publishes and environment and environment.publish_template
                and environment.work_template and environment.context
                and name_map):
            publish_items = self._process_publish_files(
                sg_publishes, environment.publish_template,
                environment.work_template, environment.context, name_map,
                environment.version_compare_ignore_fields)
        return {"publish_items": publish_items, "environment": environment}

    def _task_find_work_files(self, environment, **kwargs):
        """
        """
        #time.sleep(5)
        work_files = []
        if (environment and environment.context and environment.work_template):
            work_files = self._find_work_files(
                environment.context, environment.work_template,
                environment.version_compare_ignore_fields)
        return {"work_files": work_files}

    def _task_filter_work_files(self, work_files, environment, **kwargs):
        """
        """
        filtered_work_files = []
        if work_files:
            filtered_work_files = self._filter_work_files(
                work_files, environment.valid_file_extensions)
        return {"work_files": filtered_work_files}

    def _task_process_work_items(self, work_files, environment, name_map,
                                 **kwargs):
        """
        """
        work_items = {}
        if (work_files and environment and environment.work_template
                and environment.context and name_map):
            work_items = self._process_work_files(
                work_files, environment.work_template, environment.context,
                name_map, environment.version_compare_ignore_fields)
        return {"work_items": work_items, "environment": environment}
Example #26
0
class SiteCommunication(QtCore.QObject, CommunicationBase):
    """
    Communication channel for the site engine to the project engine.
    """

    proxy_closing = QtCore.Signal()
    proxy_created = QtCore.Signal()

    def __init__(self, engine):
        """
        :param engine: Toolkit engine.
        """
        QtCore.QObject.__init__(self)
        CommunicationBase.__init__(self, engine)

    def _create_proxy(self, pipe, authkey):
        """
        Connects to the other process's RPC server.
        """
        CommunicationBase._create_proxy(self, pipe, authkey)
        self.proxy_created.emit()

    def start_server(self):
        """
        Sets up a server to communicate with the background process.
        """
        self._create_server()

        self.register_function(self._create_proxy, "create_app_proxy")
        self.register_function(self._destroy_proxy, "destroy_app_proxy")
        self.register_function(self._proxy_log, "proxy_log")

    def _notify_proxy_closure(self):
        """
        Disconnect from the app proxy.
        """
        self.proxy_closing.emit()
        try:
            self.call_no_response("signal_disconnect")
        except Exception as e:
            logger.warning(
                "Error while sending signal to proxy to disconnect: %s", e)
        else:
            logger.debug("Proxy was signaled that we are disconnecting.")

    def _proxy_log(self, level, msg, args):
        """
        Outputs messages from the proxy into the application's logs.

        :param level: Level of the message.
        :param msg: Message to log.
        :param args: Arguments to log.
        """
        try:
            # Format first so logger.exception can use it
            msg = "[PROXY] {0}".format(msg)
            logger.log(level, msg, *args)
        except Exception:
            message = ("Unexpected error when logging proxy message: "
                       'level:%s msg:"%s" args:%s')
            logger.exception(message, level, msg, args)
Example #27
0
 def jump_to_sg(self):
     """
     Jump to shotgun, launch web browser
     """
     url = self._engine.context.shotgun_url
     QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
    def setupUi(self, FileListForm):
        FileListForm.setObjectName("FileListForm")
        FileListForm.resize(675, 632)
        self.verticalLayout = QtGui.QVBoxLayout(FileListForm)
        self.verticalLayout.setSpacing(4)
        self.verticalLayout.setContentsMargins(2, 6, 2, 2)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout_3 = QtGui.QHBoxLayout()
        self.horizontalLayout_3.setContentsMargins(1, -1, 1, -1)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.user_filter_btn = UserFilterButton(FileListForm)
        self.user_filter_btn.setStyleSheet(
            "#user_filter_btn {\n"
            "    width: 40;\n"
            "    height: 24;\n"
            "    border: 0px, none;\n"
            "    border-image: url(:/ts_multi_workfiles2/users_none.png);\n"
            "}\n"
            "#user_filter_btn::hover, #user_filter_btn::pressed {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_none_hover.png);\n"
            "}\n"
            "\n"
            "#user_filter_btn[user_style=\"none\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_none.png);\n"
            "}\n"
            "#user_filter_btn[user_style=\"current\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_current.png);\n"
            "}\n"
            "#user_filter_btn[user_style=\"other\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_other.png);\n"
            "}\n"
            "#user_filter_btn[user_style=\"all\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_all.png);\n"
            "}\n"
            "\n"
            "#user_filter_btn::hover[user_style=\"none\"], #user_filter_btn::pressed[user_style=\"none\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_none_hover.png);\n"
            "}\n"
            "#user_filter_btn::hover[user_style=\"current\"], #user_filter_btn::pressed[user_style=\"current\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_current_hover.png);\n"
            "}\n"
            "#user_filter_btn::hover[user_style=\"other\"], #user_filter_btn::pressed[user_style=\"other\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_other_hover.png);\n"
            "}\n"
            "#user_filter_btn::hover[user_style=\"all\"], #user_filter_btn::pressed[user_style=\"all\"] {\n"
            "    border-image: url(:/ts_multi_workfiles2/users_all_hover.png);\n"
            "}\n"
            "\n"
            "#user_filter_btn::menu-indicator, #user_filter_btn::menu-indicator::pressed, #user_filter_btn::menu-indicator::open {\n"
            "    left: -2px;\n"
            "    top: -2px;\n"
            "    width: 8px;\n"
            "    height: 6px;\n"
            "}\n"
            "")
        self.user_filter_btn.setText("")
        self.user_filter_btn.setFlat(True)
        self.user_filter_btn.setObjectName("user_filter_btn")
        self.horizontalLayout_3.addWidget(self.user_filter_btn)
        self.all_versions_cb = QtGui.QCheckBox(FileListForm)
        self.all_versions_cb.setObjectName("all_versions_cb")
        self.horizontalLayout_3.addWidget(self.all_versions_cb)
        spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding,
                                       QtGui.QSizePolicy.Minimum)
        self.horizontalLayout_3.addItem(spacerItem)
        self.search_ctrl = SearchWidget(FileListForm)
        self.search_ctrl.setMinimumSize(QtCore.QSize(100, 0))
        self.search_ctrl.setStyleSheet("#search_ctrl {\n"
                                       "background-color: rgb(255, 128, 0);\n"
                                       "}")
        self.search_ctrl.setObjectName("search_ctrl")
        self.horizontalLayout_3.addWidget(self.search_ctrl)
        self.horizontalLayout_3.setStretch(2, 1)
        self.verticalLayout.addLayout(self.horizontalLayout_3)
        self.view_pages = QtGui.QStackedWidget(FileListForm)
        self.view_pages.setObjectName("view_pages")
        self.list_page = QtGui.QWidget()
        self.list_page.setObjectName("list_page")
        self.horizontalLayout_5 = QtGui.QHBoxLayout(self.list_page)
        self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_5.setObjectName("horizontalLayout_5")
        self.file_list_view = GroupedListView(self.list_page)
        self.file_list_view.setStyleSheet("")
        self.file_list_view.setObjectName("file_list_view")
        self.horizontalLayout_5.addWidget(self.file_list_view)
        self.view_pages.addWidget(self.list_page)
        self.details_page = QtGui.QWidget()
        self.details_page.setObjectName("details_page")
        self.horizontalLayout_4 = QtGui.QHBoxLayout(self.details_page)
        self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")
        self.file_details_view = FileDetailsView(self.details_page)
        self.file_details_view.setObjectName("file_details_view")
        self.horizontalLayout_4.addWidget(self.file_details_view)
        self.view_pages.addWidget(self.details_page)
        self.verticalLayout.addWidget(self.view_pages)
        self.verticalLayout.setStretch(1, 1)

        self.retranslateUi(FileListForm)
        self.view_pages.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(FileListForm)
class ExternalConfigurationLoader(QtCore.QObject):
    """
    Class for loading configurations across contexts.

    **Signal Interface**

    :signal configurations_loaded(project_id, configs): Gets emitted configurations
        have been loaded for the given project. The parameters passed is the
        project id and a list of :class:`ExternalConfiguration` instances. If errors
        occurred while loading configurations, the error property will be set to a tuple
        containing the error message and the traceback, in that order.

    :signal configurations_changed(): Gets emitted whenever the class
        has detected a change to the state of shotgun which could invalidate
        any existing :class:`ExternalConfiguration` instances. This can be
        emitted at startup or typically after :meth:`refresh_shotgun_global_state`
        has been called. Any implementation which caches
        :class:`ExternalConfiguration` instances can use this signal to invalidate
        their caches.
    """

    # signal emitted to indicate that an update has been detected
    # to the pipeline configurations for a project
    configurations_loaded = QtCore.Signal(int,
                                          list)  # project_id, list of configs

    # signal to indicate that change to the configurations have been detected.
    configurations_changed = QtCore.Signal()

    # grouping used by the background task manager
    TASK_GROUP = "tk-framework-shotgunutils.external_config.ExternalConfigurationLoader"

    def __init__(self, interpreter, engine_name, plugin_id, base_config,
                 bg_task_manager, parent):
        """
        Initialize the class with the following parameters:

        .. note:: The interpreter needs to support the VFX Platform, e.g be
            able to import ``PySide`` or ``Pyside2``.

        :param str interpreter: Path to Python interpreter to use.
        :param str engine_name: Engine to run.
        :param str plugin_id: Plugin id to use when executing external requests.
        :param str base_config: Default configuration URI to use if nothing else
            is provided via Shotgun overrides.
        :param bg_task_manager: Background task manager to use for any asynchronous work.
        :type bg_task_manager: :class:`~task_manager.BackgroundTaskManager`
        :param parent: QT parent object.
        :type parent: :class:`~PySide.QtGui.QObject`
        """
        super(ExternalConfigurationLoader, self).__init__(parent)

        self._task_ids = {}

        self._plugin_id = plugin_id
        self._base_config_uri = base_config
        self._engine_name = engine_name
        self._interpreter = interpreter

        self._shotgun_state = ConfigurationState(bg_task_manager, parent)
        self._shotgun_state.state_changed.connect(
            self.configurations_changed.emit)
        # always trigger a check at startup
        self.refresh_shotgun_global_state()

        self._bg_task_manager = bg_task_manager
        self._bg_task_manager.task_completed.connect(self._task_completed)
        self._bg_task_manager.task_failed.connect(self._task_failed)

    def __repr__(self):
        """
        String representation
        """
        return "<CommandHandler %s@%s>" % (self._engine_name, self._plugin_id)

    def shut_down(self):
        """
        Shut down and deallocate.
        """
        self._shotgun_state.shut_down()

    def refresh_shotgun_global_state(self):
        """
        Requests an async refresh. If the State of Shotgun has
        changed in a way which may affect configurations, this will
        result in a ``configurations_changed`` signal being emitted.

        Examples of state changes which may affect configurations are any changes
        to related pipeline configuration, but also indirect changes such as a
        change to the list of software entities, since these can implicitly affect
        the list of commands associated with a project or entity.
        """
        self._shotgun_state.refresh()

    @property
    def engine_name(self):
        """
        The name of the engine associated with this external configuration loader.
        """
        return self._engine_name

    @property
    def interpreter(self):
        """
        The Python interpreter to when bootstrapping and loading external configurations.
        """
        return self._interpreter

    @property
    def plugin_id(self):
        """
        The plugin id which will be used when executing external requests.
        """
        return self._plugin_id

    @property
    def base_config_uri(self):
        """
        Configuration URI string to be used when nothing is provided via Shotgun overrides.
        """
        return self._base_config_uri

    @property
    def software_hash(self):
        """
        Hash string representing the state of the software
        entity in Shotgun or None if not yet determined.
        """
        return self._shotgun_state.get_software_hash()

    def request_configurations(self, project_id):
        """
        Requests a list of configuration objects for the given project.

        Emits a ``configurations_loaded`` signal when the configurations
        have been loaded.

        .. note:: If this method is called multiple times in quick succession, only
                  a single ``configurations_loaded`` signal will be emitted, belonging
                  to the last request.

        :param int project_id: Project to request configurations for.
        """
        # First of all, remove any existing requests for this project from
        # our internal task tracker. This will ensure that only one signal
        # is emitted even if this method is called multiple times
        # in rapid succession.
        #
        # note: not using a generator since we are deleting in the loop
        for task_id in self._task_ids.keys():
            if self._task_ids[task_id] == project_id:
                logger.debug(
                    "Discarding existing request_configurations request for project %s"
                    % project_id)
                del self._task_ids[task_id]

        # load existing cache file if it exists
        config_cache_key = {
            "project": project_id,
            "plugin": self._plugin_id,
            "engine": self._engine_name,
            "base_config": self._base_config_uri,
            "state_hash": self._shotgun_state.get_configuration_hash()
        }

        config_data = file_cache.load_cache(config_cache_key)
        # attempt to load configurations
        config_data_emitted = False
        if config_data:
            # got the data cached so emit it straight away
            try:
                config_objects = []
                for cfg in config_data["configurations"]:
                    config_objects.append(
                        config.deserialize(self, self._bg_task_manager, cfg))

            except ExternalConfigParseError:
                # get rid of this configuration
                file_cache.delete_cache(config_cache_key)
                logger.debug("Detected and deleted out of date cache.")

            else:
                # Check to see if any of the configs are invalid in the cache. If there
                # are, then we're going to recache in case the problematic configs in
                # Shotgun have been fixed in the interim since the cache was built.
                if [c for c in config_objects if not c.is_valid]:
                    file_cache.delete_cache(config_cache_key)
                    logger.debug(
                        "Detected an invalid config in the cache. Recaching from scratch..."
                    )
                else:
                    self.configurations_loaded.emit(project_id, config_objects)
                    config_data_emitted = True

        if not config_data_emitted:
            # Request a bg load
            unique_id = self._bg_task_manager.add_task(
                self._execute_get_configurations,
                priority=1,
                group=self.TASK_GROUP,
                task_kwargs={
                    "project_id": project_id,
                    "state_hash": self._shotgun_state.get_configuration_hash()
                })

            self._task_ids[unique_id] = project_id

    def _execute_get_configurations(self,
                                    project_id,
                                    state_hash,
                                    toolkit_manager=None):
        """
        Background task to load configs using the ToolkitManager.

        :param int project_id: Project id to load configs for.
        :param str state_hash: Hash representing the relevant
            global state of Shotgun.
        :param toolkit_manager: An optional ToolkitManager instance to use when retrieving
            pipeline configurations from Shotgun.
        :type toolkit_manager: :class:`~sgtk.bootstrap.ToolkitManager`
        :returns: Tuple with (project id, state hash, list of configs), where
            the two first items are the input parameters to this method
            and the last item is the return data from
            ToolkitManager.get_pipeline_configurations()
        """
        # get list of configurations
        mgr = toolkit_manager or sgtk.bootstrap.ToolkitManager()
        mgr.plugin_id = self._plugin_id
        configs = mgr.get_pipeline_configurations({
            "type": "Project",
            "id": project_id
        })
        return (project_id, state_hash, configs)

    def _task_completed(self, unique_id, group, result):
        """
        Called after pipeline configuration enumeration completes.

        :param str unique_id: unique task id
        :param str group: task group
        :param str result: return data from worker
        """
        if unique_id not in self._task_ids:
            return

        del self._task_ids[unique_id]

        logger.debug("Received configuration info from external process.")
        (project_id, state_hash, config_dicts) = result

        # check that the configs are complete. If not, issue warnings
        config_objects = []
        for config_dict in config_dicts:
            config_object = config.create_from_pipeline_configuration_data(
                parent=self,
                bg_task_manager=self._bg_task_manager,
                config_loader=self,
                configuration_data=config_dict)
            config_objects.append(config_object)

            if not config_object.is_valid:
                logger.debug("Configuration (%r) was found, but is invalid.",
                             config_object)

        # if no custom pipeline configs were found, we use the base config
        # note: because the base config can change over time, we make sure
        # to include it as an ingredient in the hash key below.
        if not config_dicts:
            logger.debug(
                "No configurations were found. Using the fallback configuration."
            )
            config_objects.append(
                config.create_fallback_configuration(self,
                                                     self._bg_task_manager,
                                                     self))

        # create a dictionary we can serialize
        data = {
            "project_id":
            project_id,
            "plugin_id":
            self._plugin_id,
            "global_state_hash":
            state_hash,
            "configurations":
            [config.serialize(cfg_obj) for cfg_obj in config_objects]
        }

        # save cache
        file_cache.write_cache(
            {
                "project": project_id,
                "plugin": self._plugin_id,
                "engine": self._engine_name,
                "base_config": self._base_config_uri,
                "state_hash": state_hash
            }, data)

        logger.debug("Got configuration objects for project %s: %s" %
                     (project_id, config_objects))

        self.configurations_loaded.emit(project_id, config_objects)

    def _task_failed(self, unique_id, group, message, traceback_str):
        """
        Called after pipeline configuration enumeration fails.

        :param str unique_id: unique task id
        :param str group: task group
        :param str message: Error message
        :param str traceback_str: Full traceback
        """
        if unique_id not in self._task_ids:
            return

        project_id = self._task_ids[unique_id]
        del self._task_ids[unique_id]

        logger.error("Could not determine project configurations: %s" %
                     message)

        # emit an empty list of configurations
        self.configurations_loaded.emit(project_id, [])
Example #30
0
class Section(QtGui.QWidget):
    """
    Implement common functionality for each section type.

    The widget contains a header that shows the name of the
    section and an icon button that allows to expand and collapse
    the section.

    It also contains a BaseIconList derived instance, which will
    hold all the buttons for this section.
    """

    command_triggered = QtCore.Signal(str)
    expand_toggled = QtCore.Signal(str, bool)

    def __init__(self, name, list_factory):
        """
        :param str name: Name of the section.
        :param class list_factory: Class of the list widget to instantiate.
        """
        super(Section, self).__init__(parent=None)

        self._layout = QtGui.QVBoxLayout(self)
        self.setLayout(self._layout)

        self._name = name

        # Create the header.
        self._grouping = SectionHeader()
        self._grouping.setText(name.upper())
        self._grouping.toggled.connect(self.set_expanded)
        self._layout.addWidget(self._grouping)

        # Add the list of buttons widget.
        self._list = list_factory(self)
        self._layout.addWidget(self._list)

        # Finally add a separator at the bottom.
        self._line = QtGui.QFrame()
        self._line.setFrameShape(QtGui.QFrame.HLine)
        self._line.setStyleSheet(
            "background-color: transparent; color: rgb(30, 30, 30);")
        self._line.setMidLineWidth(2)
        self._layout.addWidget(self._line)

        margins = self._layout.contentsMargins()
        margins.setTop(0)
        margins.setBottom(0)
        margins.setLeft(10)
        margins.setRight(10)
        self._layout.setContentsMargins(10, 0, 10, 0)

        # Each time a command is triggered on the list, the command_triggered
        # event is emitted.
        self._list.command_triggered.connect(self.command_triggered)

    @property
    def name(self):
        """
        Name of the section.
        """
        return self._name

    def is_expanded(self):
        """
        Return if the group is currently expanded.
        """
        return self._grouping.is_expanded()

    def set_expanded(self, checked):
        """
        Expand or collapse the group.

        :param bool checked: If True, expands the group, collapses it otherwise.
        """
        self._grouping.set_expanded(checked)
        self._list.setVisible(checked)
        self.expand_toggled.emit(self._name, checked)

    @property
    def buttons(self):
        """
        An iterator over the buttons in the section.
        """
        return self._list.buttons
def qCleanupResources():
    QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
Example #32
0
    def __init__(self):
        """
        Constructor
        """

        # first, call the base class and let it do its thing.
        QtGui.QWidget.__init__(self)

        # On Linux, in Nuke 11, we have a crash on close problem. This
        # should be safe across the board, though, so no need to limit
        # it to a specific version of Nuke. We just want to make sure
        # that panel apps, specifically shotgunpanel, have the opportunity
        # to shut down gracefully prior to application close.
        QtGui.QApplication.instance().aboutToQuit.connect(
            self._on_parent_closed)

        # pick up the rest of the construction parameters
        # these are set via the class emthod set_init_parameters()
        # because we cannot control the constructor args
        PanelClass = self._init_widget_class

        panel_id = self._init_panel_id
        args = self._init_args
        kwargs = self._init_kwargs
        bundle = self._init_bundle
        self.nuke_panel = self._nuke_panel

        # and now clear the init parameters
        self.set_init_parameters(None, None, None, None, None, None)

        bundle.logger.debug("Creating panel '%s' to host %s", panel_id,
                            PanelClass)

        # set up this object and create a layout
        self.setObjectName("%s.wrapper" % panel_id)
        self.layout = QtGui.QHBoxLayout(self)
        self.layout.setSpacing(0)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setObjectName("%s.wrapper.layout" % panel_id)

        # now loop over all widgets and look for our panel widget
        # if we find it, take it out of the layout and then
        # destroy the current container.
        # this will keep the widget around but destroy the nuke tab
        # that it was sitting in.

        self.toolkit_widget = None

        widget_name = "%s.widget" % panel_id

        for widget in QtGui.QApplication.allWidgets():

            # if the widget has got the unique widget name,
            # it's our previously created object!
            if widget.objectName() == widget_name:

                # found an existing panel widget!
                self.toolkit_widget = widget

                bundle.logger.debug("Found existing panel widget: %s",
                                    self.toolkit_widget)

                # now find the tab widget by going up the hierarchy
                tab_widget = self._find_panel_tab(self.toolkit_widget)
                if tab_widget:
                    # find the stacked widget that the tab is parented to
                    stacked_widget = tab_widget.parent()
                    if stacked_widget:
                        # and remove the tab widget completely!
                        # our widget will now be hidden
                        stacked_widget.removeWidget(tab_widget)
                        bundle.logger.debug("Removed previous panel tab %s",
                                            tab_widget)
                break

        # now check if a widget was found. If not,
        # we need to create one.
        if self.toolkit_widget is None:
            # create a new dialog
            # keep a python side reference
            # and also parent it to this widget
            self.toolkit_widget = PanelClass(*args, **kwargs)

            # give our main widget a name so that we can identify it later
            self.toolkit_widget.setObjectName(widget_name)

            bundle.logger.debug("Created new toolkit panel widget %s",
                                self.toolkit_widget)

            # now let the core apply any external stylesheets
            #
            # NOTE: To be honest, we're not entirely sure why this is required. In Nuke 12
            # we started experiencing a crash when the shotgunpanel app was being launched
            # in panel mode. On OSX that was cured with a tiny tweak to the qss itself, but
            # the problem persisted on Windows and Linux. In those cases, it does not appear
            # that anything in the qss itself was the problem, it was simply that there was
            # qss being applied AT ALL right here.
            #
            # The solution here is to defer the application of the stylesheet by 1ms, which
            # gives Qt time process other events before getting to this call. With that in
            # mind, we did attempt to just make a call to processEvents here to try to get
            # the same result without a timer, but that did not stop the crash problem.
            def _set_qss():
                bundle.engine._apply_external_styleshet(
                    bundle, self.toolkit_widget)

            self._timer = QtCore.QTimer(self.toolkit_widget)
            self._timer.setSingleShot(True)
            self._timer.timeout.connect(_set_qss)
            self._timer.start(0)
        else:
            # there is already a dialog. Re-parent it to this
            # object and move it across into this layout
            self.toolkit_widget.setParent(self)
            bundle.logger.debug("Reparented existing toolkit widget.")

        # Add the widget to our current layout
        self.layout.addWidget(self.toolkit_widget)
        bundle.logger.debug("Added toolkit widget to panel hierarchy")

        # now, the close widget logic does not propagate correctly
        # down to the child widgets. When someone closes a tab or pane,
        # QStackedWidget::removeWidget is being called, which merely takes
        # our widget out of the layout and hides it. So it will stay resident
        # in memory which is not what we want. Instead, it should close properly
        # if someone decides to close its tab.
        #
        # We can accomplish this by installing a close event listener on the
        # tab itself and have that call our widget so that we can close ourselves.
        # note that we search for the tab widget by unique id rather than going
        # up in the widget hierarchy, because the hierarchy has not been properly
        # established at this point yet.
        for widget in QtGui.QApplication.allWidgets():
            if widget.objectName() == panel_id:
                filter = CloseEventFilter(widget)
                filter.parent_closed.connect(self._on_parent_closed)
                widget.installEventFilter(filter)
                bundle.logger.debug(
                    "Installed close-event filter watcher on tab %s", widget)
                break

        # We should have a parent panel object. If we do, we can alert it to the
        # concrete sgtk panel widget we're wrapping. This will allow is to provide
        # the wrapped widget's interface to higher-level callers.
        if self.nuke_panel:
            self.nuke_panel.toolkit_widget = self.toolkit_widget