Example #1
0
class ShotgunPlaybackLabel(QtGui.QLabel):
    """
    Subclassed ``QLabel`` that displays a playback icon
    centered above its content.  
    
    While it is technically possible to use
    this label with text based content, we strongly recommend
    using it with a pixmap. Typically this is a Shotgun thumbnail.    
    
    By populating an instance with shotgun version data
    via the :meth:`set_shotgun_data()` method, the label
    will look at the data and determine whether a playback 
    icon should be displayed or not. In the case an icon is
    displayed, a playback_clicked signal may be emitted. 
    
    :signal playback_clicked(dict): The playback icon was clicked. 
        This signal passes the shotgun version data specified in
        via the :meth:`set_shotgun_data()` method back
        to the caller.   
    """
    
    # signal fires when the play button was clicked
    playback_clicked = QtCore.Signal(dict)
    
    def __init__(self, parent):
        """
        Constructor
        
        :param parent: QT parent object
        """
        QtGui.QLabel.__init__(self, parent)
        self._play_icon = QtGui.QPixmap(":/tk_framework_qtwidgets.version_label/play_icon.png")
        self._play_icon_inactive = QtGui.QPixmap(":/tk_framework_qtwidgets.version_label/play_icon_inactive.png")
        self._sg_data = None
        self._hover = False
        self._playable = False
        self._interactive = True

    def set_shotgun_data(self, sg_data):
        """
        Sets shotgun data associated with this label.
        This data will be used to drive the logic which is
        used to determine if the label should exhibit the playback icon or not.
        
        If you for example are passing a Shotgun data dictionary reprensenting
        a version, make sure to include the various quicktime and frame fields.
        
        :param sg_data: Shotgun data dictionary
        """
        self._sg_data = sg_data
        
        # based on the data, figure out if the icon should be active or not
        self._playable = False
        
        if sg_data and sg_data.get("type") == "Version":
            # versions are supported
            if sg_data.get("sg_uploaded_movie"):
                self._playable = True
        
        if self.playable and self.interactive:
            self.setCursor(QtCore.Qt.PointingHandCursor)
        else:
            self.unsetCursor() 
        
    @property    
    def playable(self):
        """
        Returns True if the label is playable given its current Shotgun data.
        """
        return self._playable

    def _get_interactive(self):
        """
        Whether a playable label is interactive. If it is not, then the play
        icon will not be overlayed on the thumbnail image, and the playback
        signal will not be emitted on click event.
        """
        return self._interactive

    def _set_interactive(self, state):
        self._interactive = bool(state)

        if self.playable and self._interactive:
            self.setCursor(QtCore.Qt.PointingHandCursor)
        else:
            self.unsetCursor() 

    interactive = QtCore.Property(
        bool,
        _get_interactive,
        _set_interactive,
    )
    
    def enterEvent(self, event):
        """
        Fires when the mouse enters the widget space
        """
        QtGui.QLabel.enterEvent(self, event)
        if self.playable and self.interactive:
            self._hover = True
            self.repaint()
        
    def leaveEvent(self, event):
        """
        Fires when the mouse leaves the widget space
        """
        QtGui.QLabel.leaveEvent(self, event)
        if self.playable and self.interactive:
            self._hover = False
            self.repaint()

    def mousePressEvent(self, event):
        """
        Fires when the mouse is pressed
        """
        QtGui.QLabel.mousePressEvent(self, event)
        if self.playable and self._hover and self.interactive:
            self.playback_clicked.emit(self._sg_data)
        
    def paintEvent(self, event):
        """
        Render the UI.
        """
        # first render the label
        QtGui.QLabel.paintEvent(self, event)
        
        if self.playable and self.interactive:
            # now render a pixmap on top
            painter = QtGui.QPainter()
            painter.begin(self)
            try:
                # set up semi transparent backdrop
                painter.setRenderHint(QtGui.QPainter.Antialiasing)
                
                # draw image
                painter.translate((painter.device().width() / 2) - (self._play_icon.width()/2), 
                                  (painter.device().height() / 2) - (self._play_icon.height()/2) )
                
                if self._hover:
                    painter.drawPixmap( QtCore.QPoint(0, 0), self._play_icon)
                else:
                    painter.drawPixmap( QtCore.QPoint(0, 0), self._play_icon_inactive)
                    
            finally:
                painter.end()
Example #2
0
class ScreenGrabber(QtGui.QDialog):
    """
    A transparent tool dialog for selecting an area (QRect) on the screen.

    This tool does not by itself perform a screen capture. The resulting
    capture rect can be used (e.g. with the get_desktop_pixmap function) to
    blit the selected portion of the screen into a pixmap.
    """

    def __init__(self, parent=None):
        """
        Constructor
        """
        super(ScreenGrabber, self).__init__(parent)

        self._opacity = 1
        self._click_pos = None
        self._capture_rect = QtCore.QRect()

        self.setWindowFlags(QtCore.Qt.FramelessWindowHint |
                            QtCore.Qt.WindowStaysOnTopHint |
                            QtCore.Qt.CustomizeWindowHint |
                            QtCore.Qt.Tool)
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
        self.setCursor(QtCore.Qt.CrossCursor)
        self.setMouseTracking(True)

        desktop = QtGui.QApplication.instance().desktop()
        desktop.resized.connect(self._fit_screen_geometry)
        desktop.screenCountChanged.connect(self._fit_screen_geometry)

    @property
    def capture_rect(self):
        """
        The resulting QRect from a previous capture operation.
        """
        return self._capture_rect

    def paintEvent(self, evt):
        """
        Paint event
        """
        # Convert click and current mouse positions to local space.
        mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
        click_pos = None
        if self._click_pos is not None:
            click_pos = self.mapFromGlobal(self._click_pos)

        painter = QtGui.QPainter(self)

        # Draw background. Aside from aesthetics, this makes the full
        # tool region accept mouse events.
        painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity))
        painter.setPen(QtCore.Qt.NoPen)
        painter.drawRect(evt.rect())

        # Clear the capture area
        if click_pos is not None:
            capture_rect = QtCore.QRect(click_pos, mouse_pos)
            painter.setCompositionMode(QtGui.QPainter.CompositionMode_Clear)
            painter.drawRect(capture_rect)
            painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)

        pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 64), 1, QtCore.Qt.DotLine)
        painter.setPen(pen)

        # Draw cropping markers at click position
        if click_pos is not None:
            painter.drawLine(evt.rect().left(), click_pos.y(),
                             evt.rect().right(), click_pos.y())
            painter.drawLine(click_pos.x(), evt.rect().top(),
                             click_pos.x(), evt.rect().bottom())

        # Draw cropping markers at current mouse position
        painter.drawLine(evt.rect().left(), mouse_pos.y(),
                         evt.rect().right(), mouse_pos.y())
        painter.drawLine(mouse_pos.x(), evt.rect().top(),
                         mouse_pos.x(), evt.rect().bottom())

    def keyPressEvent(self, evt):
        """
        Key press event
        """
        # for some reason I am not totally sure about, it looks like
        # pressing escape while this dialog is active crashes Maya.
        # I tried subclassing closeEvent, but it looks like the crashing
        # is triggered before the code reaches this point.
        # by sealing the keypress event and not allowing any further processing
        # of the escape key (or any other key for that matter), the 
        # behaviour can be successfully avoided.
        pass
    
    def mousePressEvent(self, evt):
        """
        Mouse click event
        """
        if evt.button() == QtCore.Qt.LeftButton:
            # Begin click drag operation
            self._click_pos = evt.globalPos()

    def mouseReleaseEvent(self, evt):
        """ 
        Mouse release event
        """
        if evt.button() == QtCore.Qt.LeftButton and self._click_pos is not None:
            # End click drag operation and commit the current capture rect
            self._capture_rect = QtCore.QRect(self._click_pos,
                                              evt.globalPos()).normalized()
            self._click_pos = None
        self.close()

    def mouseMoveEvent(self, evt):
        """
        Mouse move event
        """
        self.repaint()

    def showEvent(self, evt):
        """
        Show event
        """
        self._fit_screen_geometry()
        # Start fade in animation
        fade_anim = QtCore.QPropertyAnimation(self, "_opacity_anim_prop", self)
        fade_anim.setStartValue(self._opacity)
        fade_anim.setEndValue(127)
        fade_anim.setDuration(300)
        fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic)
        fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped)

    def _set_opacity(self, value):
        """
        Animation callback for opacity
        """
        self._opacity = value
        self.repaint()

    def _get_opacity(self):
        """
        Animation callback for opacity
        """
        return self._opacity

    _opacity_anim_prop = QtCore.Property(int, _get_opacity, _set_opacity)

    def _fit_screen_geometry(self):
        # Compute the union of all screen geometries, and resize to fit.
        desktop = QtGui.QApplication.instance().desktop()
        workspace_rect = QtCore.QRect()
        for i in range(desktop.screenCount()):
            workspace_rect = workspace_rect.united(desktop.screenGeometry(i))
        self.setGeometry(workspace_rect)
Example #3
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()

    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.ui.note_widget.set_bg_task_manager(task_manager)
class ScreenGrabber(QtGui.QDialog):
    """
    A transparent tool dialog for selecting an area (QRect) on the screen.

    This tool does not by itself perform a screen capture. The resulting
    capture rect can be used (e.g. with the get_desktop_pixmap function) to
    blit the selected portion of the screen into a pixmap.
    """

    # If set to a callable, it will be used when performing a
    # screen grab in place of the default behavior defined in
    # this module.
    SCREEN_GRAB_CALLBACK = None

    def __init__(self, parent=None):
        """
        Constructor
        """
        super(ScreenGrabber, self).__init__(parent)

        self._opacity = 1
        self._click_pos = None
        self._capture_rect = QtCore.QRect()

        self.setWindowFlags(QtCore.Qt.FramelessWindowHint
                            | QtCore.Qt.WindowStaysOnTopHint
                            | QtCore.Qt.CustomizeWindowHint | QtCore.Qt.Tool)
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
        self.setCursor(QtCore.Qt.CrossCursor)
        self.setMouseTracking(True)

        desktop = QtGui.QApplication.desktop()
        desktop.resized.connect(self._fit_screen_geometry)
        desktop.screenCountChanged.connect(self._fit_screen_geometry)

    @property
    def capture_rect(self):
        """
        The resulting QRect from a previous capture operation.
        """
        return self._capture_rect

    def paintEvent(self, event):
        """
        Paint event
        """
        # Convert click and current mouse positions to local space.
        mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
        click_pos = None
        if self._click_pos is not None:
            click_pos = self.mapFromGlobal(self._click_pos)

        painter = QtGui.QPainter(self)

        # Draw background. Aside from aesthetics, this makes the full
        # tool region accept mouse events.
        painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity))
        painter.setPen(QtCore.Qt.NoPen)
        painter.drawRect(event.rect())

        # Clear the capture area
        if click_pos is not None:
            capture_rect = QtCore.QRect(click_pos, mouse_pos)
            painter.setCompositionMode(QtGui.QPainter.CompositionMode_Clear)
            painter.drawRect(capture_rect)
            painter.setCompositionMode(
                QtGui.QPainter.CompositionMode_SourceOver)

        pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 64), 1, QtCore.Qt.DotLine)
        painter.setPen(pen)

        # Draw cropping markers at click position
        if click_pos is not None:
            painter.drawLine(event.rect().left(), click_pos.y(),
                             event.rect().right(), click_pos.y())
            painter.drawLine(click_pos.x(),
                             event.rect().top(), click_pos.x(),
                             event.rect().bottom())

        # Draw cropping markers at current mouse position
        painter.drawLine(event.rect().left(), mouse_pos.y(),
                         event.rect().right(), mouse_pos.y())
        painter.drawLine(mouse_pos.x(),
                         event.rect().top(), mouse_pos.x(),
                         event.rect().bottom())

    def keyPressEvent(self, event):
        """
        Key press event
        """
        # for some reason I am not totally sure about, it looks like
        # pressing escape while this dialog is active crashes Maya.
        # I tried subclassing closeEvent, but it looks like the crashing
        # is triggered before the code reaches this point.
        # by sealing the keypress event and not allowing any further processing
        # of the escape key (or any other key for that matter), the
        # behaviour can be successfully avoided.

        # TODO: See if we can get the behacior with hitting escape back
        # maybe by manually handling the closing of the window? I tried
        # some obvious things and weren't successful, but didn't dig very
        # deep as it felt like a nice-to-have and not a massive priority.

        pass

    def mousePressEvent(self, event):
        """
        Mouse click event
        """
        if event.button() == QtCore.Qt.LeftButton:
            # Begin click drag operation
            self._click_pos = event.globalPos()

    def mouseReleaseEvent(self, event):
        """
        Mouse release event
        """
        if event.button(
        ) == QtCore.Qt.LeftButton and self._click_pos is not None:
            # End click drag operation and commit the current capture rect
            self._capture_rect = QtCore.QRect(self._click_pos,
                                              event.globalPos()).normalized()
            self._click_pos = None
        self.close()

    def mouseMoveEvent(self, event):
        """
        Mouse move event
        """
        self.repaint()

    @classmethod
    def screen_capture(cls):
        """
        Modally displays the screen capture tool.

        :returns: Captured screen
        :rtype: :class:`~PySide.QtGui.QPixmap`
        """
        bundle = sgtk.platform.current_bundle()

        if cls.SCREEN_GRAB_CALLBACK:
            # use an external callback for screen grabbing
            return cls.SCREEN_GRAB_CALLBACK()

        elif sys.platform.startswith("linux"):
            # there are known issues with the QT based screen grabbing
            # on linux - some distros don't have a X11 compositing manager
            # so transparent windows aren't supported. In
            # these cases, fall back onto a traditional approach where
            # an external application is used to grab the screenshot.
            #
            # if the external application does not exist,
            # try using the QT based approach as a fallback.
            #
            # by using import first, we can advise users who have issues
            # with the qt approach to simply install imagemagick and things
            # should start to work.
            #
            pixmap = _external_screenshot()

            if pixmap is None or pixmap.isNull():
                bundle.log_debug("Falling back on internal screen grabber.")
                tool = ScreenGrabber()
                tool.exec_()
                pixmap = get_desktop_pixmap(tool.capture_rect)

            return pixmap

        elif sys.platform == "darwin":
            # With macosx there are known issues with some
            # multi-diplay setups, so better to use built-in tool
            return _external_screenshot()

        else:
            # on windows, just use the QT solution.
            tool = ScreenGrabber()
            tool.exec_()
            return get_desktop_pixmap(tool.capture_rect)

    def showEvent(self, event):
        """
        Show event
        """
        self._fit_screen_geometry()
        # Start fade in animation
        fade_anim = QtCore.QPropertyAnimation(self, "_opacity_anim_prop", self)
        fade_anim.setStartValue(self._opacity)
        fade_anim.setEndValue(127)
        fade_anim.setDuration(300)
        fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic)
        fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped)

    def _set_opacity(self, value):
        """
        Animation callback for opacity
        """
        self._opacity = value
        self.repaint()

    def _get_opacity(self):
        """
        Animation callback for opacity
        """
        return self._opacity

    _opacity_anim_prop = QtCore.Property(int, _get_opacity, _set_opacity)

    def _fit_screen_geometry(self):
        # Compute the union of all screen geometries, and resize to fit.
        desktop = QtGui.QApplication.desktop()
        workspace_rect = QtCore.QRect()
        for i in range(desktop.screenCount()):
            workspace_rect = workspace_rect.united(desktop.screenGeometry(i))
        self.setGeometry(workspace_rect)
Example #5
0
class NewItemWidget(ActivityStreamBaseWidget):
    """
    Activity stream widget that shows a UI representing a newly
    created object, for example a version or a publish.
    """
    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.
        ActivityStreamBaseWidget.__init__(self, parent)

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

        # thumbnails are hidden by default, only to appear
        # for created objects that have them set
        self.ui.details_thumb.setVisible(False)

        # make sure that click on hyperlinks bubble up
        self.ui.footer.linkActivated.connect(self._entity_request_from_url)
        self.ui.header_left.linkActivated.connect(
            self._entity_request_from_url)
        self.ui.details_thumb.playback_clicked.connect(
            lambda sg_data: self.playback_requested.emit(sg_data))
        self.ui.user_thumb.entity_requested.connect(
            lambda entity_type, entity_id: self.entity_requested.emit(
                entity_type, entity_id))

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

    @property
    def user_thumb(self):
        """
        The user thumbnail widget.
        """
        return self.ui.user_thumb

    def _get_interactive(self):
        """
        Whether the new item label is interactive, showing a play icon.
        """
        return self._interactive

    def _set_interactive(self, state):
        self._interactive = bool(state)
        self.ui.details_thumb.interactive = self._interactive

        if self._interactive:
            self.user_thumb.setCursor(QtCore.Qt.PointingHandCursor)
        else:
            self.user_thumb.setCursor(QtCore.Qt.ArrowCursor)

    interactive = QtCore.Property(bool, _get_interactive, _set_interactive)

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

    def set_info(self, data):
        """
        Populate text fields for this widget.

        Example of data:

            {'created_at': 1437322777.0,
             'created_by': {'id': 38,
                            'image': '',
                            'name': 'Manne Ohrstrom',
                            'status': 'act',
                            'type': 'HumanUser'},
             'id': 116,
             'meta': {'entity_id': 6007, 'entity_type': 'Version', 'type': 'new_entity'},
             'primary_entity': {'description': 'testing testing\n\n1\n\n2\n\n3',
                                'id': 6007,
                                'image': '',
                                'name': 'note_addressing',
                                'sg_uploaded_movie': {'content_type': 'video/quicktime',
                                                      'id': 180,
                                                      'link_type': 'upload',
                                                      'name': 'note_addressing.mov',
                                                      'type': 'Attachment',
                                                      'url': ''},
                                'status': 'rev',
                                'type': 'Version'},
             'read': False,
             'update_type': 'create'}



        :param data: data dictionary with activity stream info.
        """
        # call base class
        ActivityStreamBaseWidget.set_info(self, data)

        # make the user icon clickable
        self.ui.user_thumb.set_shotgun_data(data["created_by"])

        # set standard date and header fields
        self._set_timestamp(data, self.ui.date)

        primary_entity = data["primary_entity"]
        entity_url = self._generate_entity_url(primary_entity,
                                               this_syntax=False)

        header = "%s was created" % entity_url

        # add link if there is a link field that is populated
        if "entity" in primary_entity and primary_entity["entity"]:
            link_url = self._generate_entity_url(primary_entity["entity"])
            header += " on %s" % link_url

        self.ui.header_left.setText(header)

        # set the footer area to contain the description
        if primary_entity.get("description"):
            self.ui.footer.setText("%s" % primary_entity.get("description"))
        else:
            # hide footer fields
            self.ui.footer.setVisible(False)

        if primary_entity.get("image"):
            # there is a thumbnail. Show thumbnail.
            self.ui.details_thumb.setVisible(True)

        self.ui.details_thumb.set_shotgun_data(primary_entity)

    def apply_thumbnail(self, data):
        """
        Populate the UI with the given thumbnail

        :param image: QImage with thumbnail data
        :param thumbnail_type: thumbnail enum constant:
            ActivityStreamDataHandler.THUMBNAIL_CREATED_BY
            ActivityStreamDataHandler.THUMBNAIL_ENTITY
            ActivityStreamDataHandler.THUMBNAIL_ATTACHMENT
        """
        activity_id = data["activity_id"]

        if activity_id != self.activity_id:
            return

        thumbnail_type = data["thumbnail_type"]
        image = data["image"]

        if thumbnail_type == ActivityStreamDataHandler.THUMBNAIL_CREATED_BY:
            thumb = utils.create_round_thumbnail(image)
            self.ui.user_thumb.setPixmap(thumb)

        elif thumbnail_type == ActivityStreamDataHandler.THUMBNAIL_ENTITY:
            thumb = utils.create_rectangular_256x144_thumbnail(image)
            self.ui.details_thumb.setPixmap(thumb)