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()
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)
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)
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)