Пример #1
0
class ActionSection(flow_layout.ScrollingFlowWidget):
    '''Action list view.'''

    #: Emitted before an action is launched with action
    beforeActionLaunch = QtCore.Signal(dict, name='beforeActionLaunch')

    #: Emitted after an action has been launched with action and results
    actionLaunched = QtCore.Signal(dict, list, name='actionLaunched')

    def clear(self):
        '''Remove all actions from section.'''
        items = self.findChildren(action_item.ActionItem)
        for item in items:
            item.setParent(None)

    def addActions(self, actions):
        '''Add *actions* to section'''
        for item in actions:
            actionItem = action_item.ActionItem(item, parent=self)
            actionItem.actionLaunched.connect(self._onActionLaunched)
            actionItem.beforeActionLaunch.connect(self._onBeforeActionLaunched)
            self.addWidget(actionItem)

    def _onActionLaunched(self, action, results):
        '''Forward actionLaunched signal.'''
        self.actionLaunched.emit(action, results)

    def _onBeforeActionLaunched(self, action):
        '''Forward beforeActionLaunch signal.'''
        self.beforeActionLaunch.emit(action)
Пример #2
0
class ApplicationPlugin(QtWidgets.QWidget):
    '''Base widget for ftrack connect application plugin.'''

    #: Signal to emit to request focus of this plugin in application.
    requestApplicationFocus = QtCore.Signal(object)

    #: Signal to emit to request closing application.
    requestApplicationClose = QtCore.Signal(object)

    def getName(self):
        '''Return name of widget.'''
        return self.__class__.__name__

    def getIdentifier(self):
        '''Return identifier for widget.'''
        return self.getName().lower().replace(' ', '.')
Пример #3
0
class EntityPath(QtWidgets.QLabel):
    '''Entity path widget.'''

    path_ready = QtCore.Signal(object)

    def __init__(self, *args, **kwargs):
        '''Instantiate the entity path widget.'''
        super(EntityPath, self).__init__(*args, **kwargs)
        self.path_ready.connect(self.on_path_ready)

    @util.asynchronous
    def setEntity(self, entity):
        '''Set the *entity* for this widget.'''
        names = []
        session = entity.session
        parents = _get_entity_parents(entity)

        for entity in parents:
            if entity:
                if isinstance(entity, session.types['Project']):
                    names.append(entity['full_name'])
                else:
                    names.append(entity['name'])

        self.path_ready.emit(names)

    def on_path_ready(self, names):
        result = ' / '.join(names)
        result = 'Publish to: <b>{0}</b>'.format(result)
        self.setText(result)
Пример #4
0
class EntityPath(QtWidgets.QLineEdit):
    '''Entity path widget.'''
    path_ready = QtCore.Signal(object)

    def __init__(self, *args, **kwargs):
        '''Instantiate the entity path widget.'''
        super(EntityPath, self).__init__(*args, **kwargs)
        self.setReadOnly(True)
        self.path_ready.connect(self.on_path_ready)

    @ftrack_connect.asynchronous.asynchronous
    def setEntity(self, entity):
        '''Set the *entity* for this widget.'''
        names = []
        entities = [entity]
        try:
            entities.extend(entity.getParents())
        except AttributeError:
            pass

        for entity in entities:
            if entity:
                if isinstance(entity, ftrack.Show):
                    names.append(entity.getFullName())
                else:
                    names.append(entity.getName())

        # Reverse names since project should be first.
        names.reverse()
        self.path_ready.emit(names)

    def on_path_ready(self, names):
        '''Set current path to *names*.'''
        self.setText(' / '.join(names))
Пример #5
0
class ChatTextEdit(QtWidgets.QTextEdit):

    # Signal emitted when return is pressed on it's own.
    returnPressed = QtCore.Signal()

    def __init__(self, *args, **kwargs):
        super(ChatTextEdit, self).__init__(*args, **kwargs)

        # Install event filter at application level in order to handle
        # return pressed events
        application = QtCore.QCoreApplication.instance()
        application.installEventFilter(self)

    def eventFilter(self, obj, event):
        '''Filter *event* sent to *obj*.'''
        if obj == self:

            if event.type() == QtCore.QEvent.KeyPress:

                if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):

                    # If nativeModifiers if not equal to 0 that means return
                    # was pressed in combination with something else.
                    if event.nativeModifiers() == 0:
                        self.returnPressed.emit()
                        return True

        # Let event propagate.
        return False
Пример #6
0
class LoginServerThread(QtCore.QThread):
    '''Login server thread.'''

    # Login signal.
    loginSignal = QtCore.Signal(object, object, object)

    def start(self, url):
        '''Start thread.'''
        self.url = url
        super(LoginServerThread, self).start()

    def _handle_login(self, api_user, api_key):
        '''Login to server with *api_user* and *api_key*.'''
        self.loginSignal.emit(self.url, api_user, api_key)

    def run(self):
        '''Listen for events.'''
        self._server = BaseHTTPServer.HTTPServer(
            ('localhost', 0),
            functools.partial(
                LoginServerHandler, self._handle_login
            )
        )
        webbrowser.open_new_tab(
            '{0}/user/api_credentials?redirect_url=http://localhost:{1}'.format(
                self.url, self._server.server_port
            )
        )
        self._server.handle_request()
Пример #7
0
class ClickableLabel(QtWidgets.QLabel):
    '''Clickable label class.'''

    clicked = QtCore.Signal()

    def mousePressEvent(self, event):
        '''Override mouse press to emit signal.'''
        self.clicked.emit()
Пример #8
0
class Chat(QtWidgets.QFrame):
    '''Chat widget.'''

    chatMessageSubmitted = QtCore.Signal(object)

    def __init__(self, parent=None):
        '''Initiate chat widget with *chatHub*.'''
        super(Chat, self).__init__(parent)

        self.setLayout(QtWidgets.QVBoxLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)
        self.setObjectName('chat-widget')

        self._chatFeed = Feed(parent)
        self.layout().addWidget(self._chatFeed, stretch=1)

        self._messageArea = ChatTextEdit(self)
        self._messageArea.setMinimumHeight(30)
        self._messageArea.setMaximumHeight(75)
        self._messageArea.returnPressed.connect(self.onReturnPressed)
        self.layout().addWidget(self._messageArea, stretch=0)

        self._sendMessageButton = QtWidgets.QPushButton('Submit')
        self.layout().addWidget(self._sendMessageButton, stretch=0)

        self._sendMessageButton.clicked.connect(self.onReturnPressed)

        self.busyOverlay = ftrack_connect.ui.widget.overlay.BusyOverlay(
            self, message='Loading')
        self.hideOverlay()

    def load(self, history):
        '''Load chat *history*'''
        self._chatFeed.clearItems()
        for message in history:
            self.addMessage(message)

    def onReturnPressed(self):
        '''Handle return pressed events.'''
        text = self._messageArea.toPlainText()

        if text:
            self.chatMessageSubmitted.emit(text)
            self._messageArea.setText('')

    def addMessage(self, message):
        '''Add *message* to feed.'''
        self._chatFeed.addMessage(message)

    def showOverlay(self):
        '''Show chat overlay.'''
        self.busyOverlay.show()

    def hideOverlay(self):
        '''Show chat overlay.'''
        self.busyOverlay.hide()
Пример #9
0
class BaseField(QtWidgets.QWidget):
    '''Base widget to inherit from.'''

    #: Signal to emit on value change.
    value_changed = QtCore.Signal(object)

    @abc.abstractmethod
    def value():
        '''Return value.'''
Пример #10
0
class PanelCommunicator(QtCore.QObject):
    '''Communcator widget used to broadcast events between plugin dialogs.'''

    publishProgressSignal = QtCore.Signal(int, name='publishProgressSignal')

    def __init__(self):
        '''Initialise panel.'''
        super(PanelCommunicator, self).__init__()

        self.refListeners = []
        self.swiListeners = []
        self.infListeners = []

    def refreshListeners(self):
        '''Call all refresh listeners.'''
        for listener in self.refListeners:
            listener()

    def switchedShotListeners(self):
        '''Call all shot listeners.'''
        for listener in self.swiListeners:
            listener()

    def infoListeners(self, taskId):
        '''Call all info listeners with *taskId*.'''
        for listener in self.infListeners:
            listener(taskId)

    def addRefreshListener(self, listener):
        '''Add refresh *listener*.'''
        self.refListeners.append(listener)

    def addSwitchedShotListener(self, listener):
        '''Add switch shot *listener*.'''
        self.swiListeners.append(listener)

    def addInfoListener(self, listener):
        '''Add info *listener*.'''
        self.infListeners.append(listener)

    def emitPublishProgress(self, publishInt):
        '''Emit publish progress with *publishInt*.'''
        self.publishProgressSignal.emit(publishInt)

    def setTotalExportSteps(self, steps):
        '''Set total export *steps*.'''
        self.exportSteps = float(steps)
        self.stepsDone = float(0)

    def emitPublishProgressStep(self):
        '''Emit publish progress step.'''
        self.stepsDone += 1.0
        progress = (self.stepsDone / self.exportSteps) * 100.0
        self.publishProgressSignal.emit(int(progress))
class UncaughtError(QtWidgets.QMessageBox):
    '''Widget that handles uncaught errors and presents a message box.'''

    onError = QtCore.Signal(object, object, object)

    def __init__(self, *args, **kwargs):
        '''Initialise and setup widget.'''
        super(UncaughtError, self).__init__(*args, **kwargs)
        self.setIcon(QtWidgets.QMessageBox.Critical)
        self.onError.connect(self.exceptHook)

        # Listen to all unhandled exceptions.
        sys.excepthook = self.onError.emit

    def getTraceback(self, exceptionTraceback):
        '''Return message from *exceptionTraceback*.'''
        tracebackInfoStream = cStringIO.StringIO()
        traceback.print_tb(exceptionTraceback, None, tracebackInfoStream)
        tracebackInfoStream.seek(0)
        return tracebackInfoStream.read()

    def exceptHook(self, exceptionType, exceptionValue, exceptionTraceback):
        '''Show message box with error details.'''

        logging.error('Logging an uncaught exception',
                      exc_info=(exceptionType, exceptionValue,
                                exceptionTraceback))

        # Show exception to user.
        tracebackInfo = self.getTraceback(exceptionTraceback)
        self.setDetailedText(tracebackInfo)

        # Make sure text is at least a certain length to force message box size.
        # Otherwise buttons will not fit.
        self.setText(str(exceptionValue).ljust(50, ' '))
        self.exec_()

    def resizeEvent(self, event):
        '''Hook into the resize *event* and force width of detailed text.'''
        result = super(UncaughtError, self).resizeEvent(event)

        detailsBox = self.findChild(QtWidgets.QTextEdit)
        if detailsBox is not None:
            detailsBox.setFixedSize(500, 200)

        return result
Пример #12
0
class SignalConversationHub(ConversationHub, QtCore.QObject):
    '''Crew hub using Qt signals.'''

    #: Signal to emit on heartbeat.
    onHeartbeat = QtCore.Signal(object)

    #: Signal to emit on presence enter event.
    onEnter = QtCore.Signal(object)

    #: Signal to emit on presence exit event.
    onExit = QtCore.Signal(object)

    #: Signal to emit on conversations messagages loaded.
    onConversationMessagesLoaded = QtCore.Signal(object, object)

    #: Signal to emit on conversations messagages loaded.
    onConversationUpdated = QtCore.Signal(object)

    #: Signal to emit on conversation seen.
    onConversationSeen = QtCore.Signal(object)

    def _onHeartbeat(self, event):
        '''Increase subscription time for *event*.'''
        super(SignalConversationHub, self)._onHeartbeat(event)
        self.onHeartbeat.emit(event['data'])

    def _onEnter(self, data):
        '''Handle enter events.'''
        self.onEnter.emit(data)

    def _onExit(self, data):
        '''Handle exit events.'''
        self.onExit.emit(data)

    def _onConversationMessagesLoaded(self, conversationId, messages):
        '''Handle messages loaded for conversation.'''
        self.onConversationMessagesLoaded.emit(conversationId, messages)

    def _onConversationUpdated(self, conversationId):
        '''Handle conversation with *conversationId* updated.'''
        self.logger.debug('onConversationUpdated emitted for {0}'.format(
            conversationId
        ))
        self.onConversationUpdated.emit(conversationId)

    def _onConversationSeen(self, event):
        '''Handle conversation seen event.'''
        self.logger.debug('Handle conversation seen event: {0}'.format(
            event['data']
        ))

        self._conversations[event['data']['conversation']] = []
        self.onConversationSeen.emit(event['data'])
Пример #13
0
class GlobalSwitchDialog(QtWidgets.QDialog):
    '''Global context switch tool.'''

    # Emitted when context changes.
    context_changed = QtCore.Signal(object)

    def __init__(self, current_entity):
        '''Initialize GlobalSwitchDialog with *current_entity*.'''
        super(GlobalSwitchDialog, self).__init__()
        self.setWindowTitle('Global Context Switch')
        layout = QtWidgets.QVBoxLayout()
        self._session = current_entity.session
        self.setLayout(layout)
        self._entity_browser = context_selector.EntityBrowser()
        layout.addWidget(self._entity_browser)
        current_location = [e['id'] for e in current_entity['link']]
        self._entity_browser.setLocation(current_location)
        self._entity_browser.accepted.connect(self.on_context_changed)
        self._entity_browser.rejected.connect(self.close)
        self.context_changed.connect(self.on_notify_user)

    def on_context_changed(self):
        '''Handle context change event.'''
        selected_entity = self._entity_browser.selected()[0]
        self.close()
        self.context_changed.emit(selected_entity['id'])

    def on_notify_user(self, context_id):
        '''Handle user notification on context change event.'''
        context = self._session.get('Context', context_id)
        parents = ' / '.join([c['name'] for c in context['link']])

        QtWidgets.QMessageBox.information(
            self,
            'Context Changed',
            u'You have now changed context to: {0}'.format(parents)
        )
Пример #14
0
class Actions(QtWidgets.QWidget):
    '''Actions widget. Displays and runs actions with a selectable context.'''

    RECENT_METADATA_KEY = 'ftrack_recent_actions'
    RECENT_ACTIONS_LENGTH = 20
    ACTION_LAUNCH_MESSAGE_TIMEOUT = 1

    #: Emitted when recent actions has been modified
    recentActionsChanged = QtCore.Signal(name='recentActionsChanged')

    def __init__(self, parent=None):
        '''Initiate a actions view.'''
        super(Actions, self).__init__(parent)

        self.logger = logging.getLogger(__name__ + '.' +
                                        self.__class__.__name__)
        self._session = ftrack_connect.session.get_session()

        layout = QtWidgets.QVBoxLayout()
        self.setLayout(layout)

        self._currentUserId = None
        self._recentActions = []
        self._actions = []

        self._entitySelector = entity_selector.EntitySelector()
        self._entitySelector.setFixedHeight(50)
        self._entitySelector.entityChanged.connect(self._onEntityChanged)
        layout.addWidget(QtWidgets.QLabel('Select action context'))
        layout.addWidget(self._entitySelector)

        self._recentLabel = QtWidgets.QLabel('Recent')
        layout.addWidget(self._recentLabel)
        self._recentSection = ActionSection(self)
        self._recentSection.setFixedHeight(100)
        self._recentSection.beforeActionLaunch.connect(
            self._onBeforeActionLaunched)
        self._recentSection.actionLaunched.connect(self._onActionLaunched)
        layout.addWidget(self._recentSection)

        self._allLabel = QtWidgets.QLabel('Discovering actions..')
        self._allLabel.setAlignment(QtCore.Qt.AlignCenter)
        layout.addWidget(self._allLabel)
        self._allSection = ActionSection(self)
        self._allSection.beforeActionLaunch.connect(
            self._onBeforeActionLaunched)
        self._allSection.actionLaunched.connect(self._onActionLaunched)
        layout.addWidget(self._allSection)

        self._overlay = overlay.BusyOverlay(self, message='Launching...')
        self._overlay.setVisible(False)

        self.recentActionsChanged.connect(self._updateRecentSection)

        self._loadActionsForContext([])
        self._updateRecentActions()

    def _onBeforeActionLaunched(self, action):
        '''Before action is launched, show busy overlay with message..'''
        self.logger.debug(u'Before action launched: {0}'.format(action))
        message = u'Launching action <em>{0} {1}</em>...'.format(
            action.get('label', 'Untitled action'), action.get('variant', ''))
        self._overlay.setMessage(message)
        self._overlay.indicator.show()
        self._overlay.setVisible(True)

    def _onActionLaunched(self, action, results):
        '''On action launched, save action and add it to top of list.'''
        self.logger.debug(u'Action launched: {0}'.format(action))
        if self._isRecentActionsEnabled():
            self._addRecentAction(action['label'])
            self._moveToFront(self._recentActions, action['label'])
            self._updateRecentSection()

        self._showResultMessage(results)

        validMetadata = [
            'actionIdentifier', 'label', 'variant', 'applicationIdentifier'
        ]
        metadata = {}
        for key, value in action.items():
            if key in validMetadata and value is not None:
                metadata[key] = value

        # Send usage event in the main thread to prevent X server threading
        # related crashes on Linux.
        ftrack_connect.usage.send_event('LAUNCHED-ACTION',
                                        metadata,
                                        asynchronous=False)

    def _showResultMessage(self, results):
        '''Show *results* message in overlay.'''
        message = 'Launched action'
        try:
            result = results[0]
            if 'items' in result.keys():
                message = (
                    'Custom UI for actions is not yet supported in Connect.')
            else:
                message = result['message']
        except Exception:
            pass

        self._overlay.indicator.stop()
        self._overlay.indicator.hide()
        self._overlay.setMessage(message)
        self._hideOverlayAfterTimeout(self.ACTION_LAUNCH_MESSAGE_TIMEOUT)

    def _hideOverlayAfterTimeout(self, timeout):
        '''Hide overlay after *timeout* seconds.'''
        QtCore.QTimer.singleShot(
            timeout * 1000, functools.partial(self._overlay.setVisible, False))

    def _onEntityChanged(self, entity):
        '''Load new actions when the context has changed'''
        context = []
        try:
            context = [{
                'entityId': entity.get('entityId'),
                'entityType': entity.get('entityType'),
            }]
            self.logger.debug(u'Context changed: {0}'.format(context))
        except Exception:
            self.logger.debug(u'Invalid entity: {0}'.format(entity))

        self._recentSection.clear()
        self._allSection.clear()
        self._loadActionsForContext(context)

    def _updateRecentSection(self):
        '''Clear and update actions in recent section.'''
        self._recentSection.clear()
        recentActions = []
        for recentAction in self._recentActions:
            for action in self._actions:
                if action[0]['label'] == recentAction:
                    recentActions.append(action)

        if recentActions:
            self._recentSection.addActions(recentActions)
            self._recentLabel.show()
            self._recentSection.show()
        else:
            self._recentLabel.hide()
            self._recentSection.hide()

    def _updateAllSection(self):
        '''Clear and update actions in all section.'''
        self._allSection.clear()
        if self._actions:
            self._allSection.addActions(self._actions)
            self._allLabel.setAlignment(QtCore.Qt.AlignLeft)
            self._allLabel.setText('All actions')
        else:
            self._allLabel.setAlignment(QtCore.Qt.AlignCenter)
            self._allLabel.setText(
                '<h2 style="font-weight: normal">No actions found</h2>'
                '<p>Try another selection or add some actions.</p>')

    @ftrack_connect.asynchronous.asynchronous
    def _updateRecentActions(self):
        '''Retrieve and update recent actions.'''
        self._recentActions = self._getRecentActions()
        self.recentActionsChanged.emit()

    def _isRecentActionsEnabled(self):
        '''Return if recent actions is enabled.

        Recent actions depends on being able to save metadata on users,
        which was added in a ftrack server version 3.2.x. Check for the
        metadata attribute on the dynamic class.
        '''
        userHasMetadata = any(
            attribute.name == 'metadata'
            for attribute in self._session.types['User'].attributes)
        return userHasMetadata

    def _getCurrentUserId(self):
        '''Return current user id.'''
        if not self._currentUserId:

            user = self._session.query('User where username="******"'.format(
                self._session.api_user)).one()
            self._currentUserId = user['id']

        return self._currentUserId

    def _getRecentActions(self):
        '''Retrieve recent actions from the server.'''
        if not self._isRecentActionsEnabled():
            return []

        metadata = self._session.query(
            'Metadata where key is "{0}" and parent_type is "User" '
            'and parent_id is "{1}"'.format(self.RECENT_METADATA_KEY,
                                            self._getCurrentUserId())).first()

        recentActions = []
        if metadata:
            try:
                recentActions = json.loads(metadata['value'])
            except ValueError as error:
                self.logger.warning(
                    'Error parsing metadata: {0}'.format(metadata))
        return recentActions

    def _moveToFront(self, itemList, item):
        '''Prepend or move *item* to front of *itemList*.'''
        try:
            itemList.remove(item)
        except ValueError:
            pass
        itemList.insert(0, item)

    @ftrack_connect.asynchronous.asynchronous
    def _addRecentAction(self, actionLabel):
        '''Add *actionLabel* to recent actions, persisting the change.'''
        recentActions = self._getRecentActions()
        self._moveToFront(recentActions, actionLabel)
        recentActions = recentActions[:self.RECENT_ACTIONS_LENGTH]
        encodedRecentActions = json.dumps(recentActions)

        self._session.ensure(
            'Metadata', {
                'parent_type': 'User',
                'parent_id': self._getCurrentUserId(),
                'key': self.RECENT_METADATA_KEY,
                'value': encodedRecentActions
            },
            identifying_keys=['parent_type', 'parent_id', 'key'])

    def _loadActionsForContext(self, context):
        '''Obtain new actions synchronously for *context*.'''
        discoveredActions = []

        results = ftrack.EVENT_HUB.publish(ftrack.Event(
            topic='ftrack.action.discover', data=dict(selection=context)),
                                           synchronous=True)

        for result in results:
            if result:
                for action in result.get('items', []):
                    discoveredActions.append(
                        ActionBase(action, is_new_api=False))

        session = ftrack_connect.session.get_shared_session()
        results = session.event_hub.publish(ftrack_api.event.base.Event(
            topic='ftrack.action.discover', data=dict(selection=context)),
                                            synchronous=True)

        for result in results:
            if result:
                for action in result.get('items', []):
                    discoveredActions.append(
                        ActionBase(action, is_new_api=True))

        # Sort actions by label
        groupedActions = []
        for action in discoveredActions:
            action['selection'] = context
            added = False
            for groupedAction in groupedActions:
                if action['label'] == groupedAction[0]['label']:
                    groupedAction.append(action)
                    added = True

            if not added:
                groupedActions.append([action])

        # Sort actions by label
        groupedActions = sorted(
            groupedActions,
            key=lambda groupedAction: groupedAction[0]['label'].lower())

        self.logger.debug('Discovered actions: {0}'.format(groupedActions))
        self._actions = groupedActions
        self._updateRecentSection()
        self._updateAllSection()
Пример #15
0
class Notification(QtWidgets.QWidget):
    '''In-App Notification widget.'''

    #: Signal that a loading operation has started.
    loadStarted = QtCore.Signal()

    #: Signal that a loading operation has ended.
    loadEnded = QtCore.Signal()

    def __init__(self, parent=None):
        '''Initialise widget with *parent*'''
        super(Notification, self).__init__(parent=parent)

        self._context = defaultdict(list)

        layout = QtWidgets.QVBoxLayout()

        toolbar = QtWidgets.QHBoxLayout()

        self.setLayout(layout)

        reloadIcon = QtGui.QIcon(QtGui.QPixmap(':/ftrack/image/dark/reload'))

        self.reloadButton = QtWidgets.QPushButton(reloadIcon, '')
        self.reloadButton.clicked.connect(self.reload)

        toolbar.addWidget(QtWidgets.QWidget(), stretch=1)
        toolbar.addWidget(self.reloadButton, stretch=0)

        layout.addLayout(toolbar)

        self._list = NotificationList(self)
        self._list.setObjectName('notification-list')
        layout.addWidget(self._list, stretch=1)

        self.overlay = ftrack_connect.ui.widget.overlay.BusyOverlay(
            self, message='Loading')

        self.overlay.hide()

        self.loadStarted.connect(self._onLoadStarted)
        self.loadEnded.connect(self._onLoadEnded)

    def _onLoadStarted(self):
        '''Handle load started.'''
        self.reloadButton.setEnabled(False)
        self.overlay.show()

    def _onLoadEnded(self):
        '''Handle load ended.'''
        self.overlay.hide()
        self.reloadButton.setEnabled(True)

    def addNotification(self, notification, row=None):
        '''Add *notification* on *row*.'''
        self._list.addItem(notification, row)

    def addContext(self, contextId, contextType, _reload=True):
        '''Add context with *contextId* and *contextType*.

        Optional *_reload* can be set to False to prevent reload of widget.

        '''

        self._context[contextType].append(contextId)

        if _reload:
            self.reload()

    def clearContext(self, _reload=True):
        '''Clear context and *_reload*.'''
        self._context = defaultdict(list)

        if _reload:
            self.reload()

    def removeContext(self, contextId, _reload=True):
        '''Remove context with *contextId*.'''

        for contextType, contextIds in self._context.items():
            if contextId in contextIds:
                contextIds.remove(contextId)

                self._context[contextType] = contextIds
                break

        if _reload:
            self.reload()

    def clear(self):
        '''Clear list of notifications.'''
        self._list.clearItems()

    def _reload(self):
        '''Return events based on current context.'''
        events = []
        response = ftrack.EVENT_HUB.publish(ftrack.Event(
            'ftrack.crew.notification.get-events',
            data=dict(context=self._context)),
                                            synchronous=True)

        if len(response):
            events = response[0]

        return events

    def reload(self):
        '''Reload notifications.'''
        self.loadStarted.emit()
        worker = ftrack_connect.worker.Worker(self._reload)
        worker.start()

        while worker.isRunning():
            app = QtWidgets.QApplication.instance()
            app.processEvents()

        if worker.error:
            self.loadEnded.emit()
            raise worker.error[1], None, worker.error[2]

        events = worker.result

        self.clear()
        for event in events:
            self.addNotification({
                'type': _typeMapper[event['action']],
                'event': event
            })

        self.loadEnded.emit()
Пример #16
0
class EntityTreeModel(QtCore.QAbstractItemModel):
    '''Model representing entity tree.'''

    #: Role referring to :class:`Item` instance.
    ITEM_ROLE = QtCore.Qt.UserRole + 1

    #: Role referring to the unique identity of :class:`Item`.
    IDENTITY_ROLE = ITEM_ROLE + 1

    #: Signal that a loading operation has started.
    loadStarted = QtCore.Signal()

    #: Signal that a loading operation has ended.
    loadEnded = QtCore.Signal()

    def __init__(self, root=None, parent=None):
        '''Initialise with *root* entity and optional *parent*.'''
        super(EntityTreeModel, self).__init__(parent=parent)
        self.root = root
        self.columns = ['Name', 'Type']

    def rowCount(self, parent):
        '''Return number of children *parent* index has.'''
        if parent.column() > 0:
            return 0

        if parent.isValid():
            item = parent.internalPointer()
        else:
            item = self.root

        return len(item.children)

    def columnCount(self, parent):
        '''Return amount of data *parent* index has.'''
        return len(self.columns)

    def flags(self, index):
        '''Return flags for *index*.'''
        if not index.isValid():
            return QtCore.Qt.NoItemFlags

        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

    def index(self, row, column, parent=None):
        '''Return index for *row* and *column* under *parent*.'''
        if parent is None:
            parent = QtCore.QModelIndex()

        if not self.hasIndex(row, column, parent):
            return QtCore.QModelIndex()

        if not parent.isValid():
            item = self.root
        else:
            item = parent.internalPointer()

        try:
            child = item.children[row]
        except IndexError:
            return QtCore.QModelIndex()
        else:
            return self.createIndex(row, column, child)

    def parent(self, index):
        '''Return parent of *index*.'''
        if not index.isValid():
            return QtCore.QModelIndex()

        item = index.internalPointer()
        if not item:
            return QtCore.QModelIndex()

        parent = item.parent
        if not parent or parent == self.root:
            return QtCore.QModelIndex()

        return self.createIndex(parent.row, 0, parent)

    def match(self, *args, **kwargs):
        return super(EntityTreeModel, self).match(*args, **kwargs)

    def item(self, index):
        '''Return item at *index*.'''
        return self.data(index, role=self.ITEM_ROLE)

    def icon(self, index):
        '''Return icon for index.'''
        return self.data(index, role=QtCore.Qt.DecorationRole)

    def data(self, index, role):
        '''Return data for *index* according to *role*.'''
        if not index.isValid():
            return None

        column = index.column()
        item = index.internalPointer()

        if role == self.ITEM_ROLE:
            return item

        elif role == self.IDENTITY_ROLE:
            return item.id

        elif role == QtCore.Qt.DisplayRole:
            column_name = self.columns[column]

            if column_name == 'Name':
                return item.name
            elif column_name == 'Type':
                return item.type

        elif role == QtCore.Qt.DecorationRole:
            if column == 0:
                return item.icon

        return None

    def headerData(self, section, orientation, role):
        '''Return label for *section* according to *orientation* and *role*.'''
        if orientation == QtCore.Qt.Horizontal:
            if section < len(self.columns):
                column = self.columns[section]
                if role == QtCore.Qt.DisplayRole:
                    return column

        return None

    def hasChildren(self, index):
        '''Return if *index* has children.

        Optimised to avoid loading children at this stage.

        '''
        if not index.isValid():
            item = self.root
        else:
            item = index.internalPointer()
            if not item:
                return False

        return item.mayHaveChildren()

    def canFetchMore(self, index):
        '''Return if more data available for *index*.'''
        if not index.isValid():
            item = self.root
        else:
            item = index.internalPointer()

        return item.canFetchMore()

    def fetchMore(self, index):
        '''Fetch additional data under *index*.

        Loading is done in a background thread with UI events continually
        processed to maintain a responsive interface.

        :attr:`EntityTreeModel.loadStarted` is emitted at start of load with
        :attr:`EntityTreeModel.loadEnded` emitted when load completes.

        '''
        if not index.isValid():
            item = self.root
        else:
            item = index.internalPointer()

        if item.canFetchMore():
            self.loadStarted.emit()
            startIndex = len(item.children)

            worker = util.Worker(item.fetchChildren)
            worker.start()

            while worker.isRunning():
                app = QtWidgets.QApplication.instance()
                app.processEvents()

            if worker.error:
                raise worker.error[1], None, worker.error[2]

            additionalChildren = worker.result

            endIndex = startIndex + len(additionalChildren) - 1
            if endIndex >= startIndex:
                self.beginInsertRows(index, startIndex, endIndex)
                for newChild in additionalChildren:
                    item.addChild(newChild)
                self.endInsertRows()

            self.loadEnded.emit()

    def reloadChildren(self, index):
        '''Reload the children of parent *index*.'''
        if not self.hasChildren(index):
            return

        if not index.isValid():
            item = self.root
        else:
            item = index.internalPointer()

        self.beginRemoveRows(index, 0, self.rowCount(index))
        item.clearChildren()
        self.endRemoveRows()

    def reset(self):
        '''Reset model'''
        self.beginResetModel()
        self.root.clearChildren()
        self.endResetModel()
Пример #17
0
class FtrackImportAssetDialog(QtWidgets.QDialog):
    '''Import asset dialog widget.'''

    importSignal = QtCore.Signal()

    def __init__(self, parent=None, connector=None):
        '''Instantiate widget with *connector*.'''
        if not connector:
            raise ValueError(
                'Please provide a connector object for {0}'.format(
                    self.__class__.__name__))

        super(FtrackImportAssetDialog, self).__init__(parent=parent)
        applyTheme(self, 'integration')

        #---------------------------------------------------------------------------#
        # Add for proper hou colors
        try:
            import hou
            self.stylesheet = '%s; background-color: #3a3a3a; color: #FFFFFF;' % hou.qt.styleSheet(
            )
            self.setStyleSheet(self.stylesheet)
        except:
            pass
#---------------------------------------------------------------------------#

        self.connector = connector
        self.currentEntity = ftrack.Task(
            os.getenv('FTRACK_TASKID', os.getenv('FTRACK_SHOTID')))

        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                           QtWidgets.QSizePolicy.Expanding)

        self.setMinimumWidth(600)

        self.mainLayout = QtWidgets.QVBoxLayout(self)
        self.setLayout(self.mainLayout)

        self.mainLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.setSpacing(0)

        self.scrollArea = QtWidgets.QScrollArea(self)
        self.mainLayout.addWidget(self.scrollArea)

        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        self.scrollArea.setLineWidth(0)
        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.scrollArea.setHorizontalScrollBarPolicy(
            QtCore.Qt.ScrollBarAlwaysOff)

        self.mainWidget = QtWidgets.QWidget(self)
        self.scrollArea.setWidget(self.mainWidget)

        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.mainWidget.setLayout(self.verticalLayout)

        self.headerWidget = header.Header(getpass.getuser(), self)
        self.verticalLayout.addWidget(self.headerWidget, stretch=0)

        self.browseTasksWidget = ContextSelector(
            currentEntity=self.currentEntity, parent=self)

        self.verticalLayout.addWidget(self.browseTasksWidget, stretch=0)

        self.listAssetsTableWidget = ListAssetsTableWidget(self)

        self.verticalLayout.addWidget(self.listAssetsTableWidget, stretch=4)

        # Horizontal line
        self.divider = QtWidgets.QFrame()
        self.divider.setFrameShape(QtWidgets.QFrame.HLine)
        self.divider.setFrameShadow(QtWidgets.QFrame.Sunken)
        self.divider.setLineWidth(2)

        self.verticalLayout.addWidget(self.divider)

        self.assetVersionDetailsWidget = AssetVersionDetailsWidget(
            self, connector=self.connector)

        self.verticalLayout.addWidget(self.assetVersionDetailsWidget,
                                      stretch=0)

        self.componentTableWidget = ComponentTableWidget(
            self, connector=self.connector)

        self.verticalLayout.addWidget(self.componentTableWidget, stretch=3)

        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.verticalLayout.addLayout(self.horizontalLayout)

        self.importAllButton = QtWidgets.QPushButton("Import All")
        self.importAllButton.setFixedWidth(120)
        self.importAllButton.setObjectName('ftrack-import-btn')

        self.importSelectedButton = QtWidgets.QPushButton("Import Selected")
        self.importSelectedButton.setFixedWidth(120)
        self.importAllButton.setObjectName('ftrack-import-btn')

        self.horizontalLayout.addWidget(self.importSelectedButton)
        self.horizontalLayout.addWidget(self.importAllButton)

        self.horizontalLayout.setAlignment(QtCore.Qt.AlignRight)

        self.importOptionsWidget = ImportOptionsWidget(
            parent=self, connector=self.connector)

        self.verticalLayout.addWidget(self.importOptionsWidget, stretch=0)

        self.messageLabel = QtWidgets.QLabel(self)
        self.messageLabel.setText(' \n ')
        self.verticalLayout.addWidget(self.messageLabel, stretch=0)

        self.setObjectName('ftrackImportAsset')
        self.setWindowTitle("ftrackImportAsset")

        panelComInstance = PanelComInstance.instance()
        panelComInstance.addSwitchedShotListener(self.reset_context_browser)

        self.browseTasksWidget.entityChanged.connect(self.clickedIdSignal)

        self.importAllButton.clicked.connect(self.importAllComponents)
        self.importSelectedButton.clicked.connect(
            self.importSelectedComponents)
        self.listAssetsTableWidget.assetVersionSelectedSignal[str].connect(
            self.clickedAssetVSignal)
        self.listAssetsTableWidget.assetTypeSelectedSignal[str].connect(
            self.importOptionsWidget.setStackedWidget)
        self.importSignal.connect(panelComInstance.refreshListeners)

        self.componentTableWidget.importComponentSignal.connect(
            self.onImportComponent)

        self.browseTasksWidget.reset()

    def reset_context_browser(self):
        '''Reset task browser to the value stored in the environments'''
        entity_id = os.getenv('FTRACK_TASKID', os.getenv('FTRACK_SHOTID'))
        entity = ftrack.Task(entity_id)
        self.browseTasksWidget.reset(entity)

    def importSelectedComponents(self):
        '''Import selected components.'''
        selectedRows = self.componentTableWidget.selectionModel().selectedRows(
        )
        for r in selectedRows:
            self.onImportComponent(r.row())

    def importAllComponents(self):
        '''Import all components.'''
        rowCount = self.componentTableWidget.rowCount()
        for i in range(rowCount):
            self.onImportComponent(i)

    def onImportComponent(self, row):
        '''Handle importing component.'''
        importOptions = self.importOptionsWidget.getOptions()

        # TODO: Add methods to panels to ease retrieval of this data
        componentItem = self.componentTableWidget.item(
            row, self.componentTableWidget.columns.index('Component'))
        component_id = componentItem.data(
            self.componentTableWidget.COMPONENT_ROLE)

        ftrack_component = self.connector.session.query(
            'select name, version.asset.type.short, version.asset.name, '
            'version.asset.type.name, version.asset.versions.version, '
            'version.id, version.version, version.asset.versions, '
            'version.date, version.comment, version.asset.name, version, '
            'version_id, version.user.first_name, version.user.last_name '
            ' from Component where id is {0}'.format(component_id)).one()

        assetVersion = ftrack_component['version']['id']

        self.importSignal.emit()
        asset_name = ftrack_component['version']['asset']['name']

        accessPath = self.componentTableWidget.item(
            row, self.componentTableWidget.columns.index('Path')).text()

        importObj = FTAssetObject(componentId=ftrack_component['id'],
                                  filePath=accessPath,
                                  componentName=ftrack_component['name'],
                                  assetVersionId=assetVersion,
                                  options=importOptions)
        try:
            self.connector.importAsset(importObj)
        except Exception, error:
            self.headerWidget.setMessage(str(error.message), 'error')
            return

        self.headerWidget.setMessage(u'Imported {0}.{1}:v{2}'.format(
            asset_name, ftrack_component['name'],
            ftrack_component['version']['version']))
class TimeLogList(ftrack_connect.ui.widget.item_list.ItemList):
    '''List time logs widget.'''

    itemSelected = QtCore.Signal(object)

    def __init__(self, parent=None, title=None, headerWidgets=None):
        '''Instantiate widget with optional *parent* and *title*.

        *headerWidgets* is an optional list of widgets to append to the header
        of the time log widget.

        '''
        super(TimeLogList,
              self).__init__(widgetFactory=self._createWidget,
                             widgetItem=lambda widget: widget.value(),
                             parent=parent)
        self.setObjectName('time-log-list')
        self.list.setShowGrid(False)

        # Disable selection on internal list.
        self.list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)

        headerLayout = QtWidgets.QHBoxLayout()
        self.titleLabel = QtWidgets.QLabel(title)
        self.titleLabel.setProperty('title', True)

        headerLayout.addWidget(self.titleLabel, stretch=1)

        # TODO: Refacor and make use of QToolBar and QAction.
        # Also consider adding 'addAction'/'removeAction'.
        if headerWidgets:
            for widget in headerWidgets:
                headerLayout.addWidget(widget, stretch=0)

        self.layout().insertLayout(0, headerLayout)

    def setTitle(self, title):
        '''Set *title*.'''
        self.titleLabel.setText(title)

    def title(self):
        '''Return current title.'''
        self.titleLabel.text()

    def addItem(self, item, row=None):
        '''Add *item* at *row*.

        If *row* not specified then add to end of list.

        Return row item added at.

        '''
        row = super(TimeLogList, self).addItem(item, row=row)
        widget = self.list.widgetAt(row)

        # Connect the widget's selected signal to the itemSelected signal
        widget.selected.connect(self.itemSelected.emit)

        return row

    def _createWidget(self, item):
        '''Return time log widget for *item*.

        *item* should be a mapping of keyword arguments to pass to
        :py:class:`ftrack_connect.ui.widget.time_log.TimeLog`.

        '''
        if item is None:
            item = {}

        return ftrack_connect.ui.widget.time_log.TimeLog(**item)
Пример #19
0
class ItemList(QtWidgets.QFrame):
    '''Manage a list of items represented by widgets.'''

    itemsChanged = QtCore.Signal()

    def __init__(self, widgetFactory, widgetItem, parent=None):
        '''Initialise widget with *parent*.

        *widgetFactory* should be a callable that accepts an item and returns
        an appropriate widget.

        *widgetItem* should be a callable that accepts a widget and returns
        the appropriate item from the widget.

        '''
        self.widgetFactory = widgetFactory
        self.widgetItem = widgetItem

        super(ItemList, self).__init__(parent=parent)

        self.setLayout(QtWidgets.QVBoxLayout())
        self.setFrameStyle(QtWidgets.QFrame.StyledPanel
                           | QtWidgets.QFrame.NoFrame)

        # List
        self.list = ftrack_connect.ui.widget.list.List()
        self.layout().addWidget(self.list, stretch=1)

        self.layout().setContentsMargins(5, 5, 5, 5)

    def count(self):
        '''Return count of items in list.'''
        return self.list.count()

    def addItem(self, item, row=None):
        '''Add *item* at *row*.

        If *row* not specified then add to end of list.

        Return row item added at.

        '''
        widget = self.widgetFactory(item)
        row = self.list.addWidget(widget, row)
        self.itemsChanged.emit()

        return row

    def removeItem(self, row):
        '''Remove item at *row*.'''
        self.list.removeWidget(row)
        self.itemsChanged.emit()

    def clearItems(self):
        '''Remove all items.'''
        self.list.clearWidgets()
        self.itemsChanged.emit()

    def indexOfItem(self, item):
        '''Return row of *item* in list or None if not present.'''
        index = None

        for row in range(self.count()):
            widget = self.list.widgetAt(row)
            if self.widgetItem(widget) == item:
                index = row
                break

        return index

    def items(self):
        '''Return list of items.'''
        items = []
        for row in range(self.count()):
            widget = self.list.widgetAt(row)
            items.append(self.widgetItem(widget))

        return items

    def itemAt(self, row):
        '''Return item at *row*.'''
        widget = self.list.widgetAt(row)
        return self.widgetItem(widget)
class EntitySelector(QtWidgets.QStackedWidget):
    '''Entity selector widget.'''

    entityChanged = QtCore.Signal(object)

    def __init__(self, *args, **kwargs):
        '''Instantiate the entity selector widget.'''
        super(EntitySelector, self).__init__(*args, **kwargs)
        self._entity = None

        # Create widget used to select an entity.
        selectionWidget = QtWidgets.QFrame()
        selectionWidget.setLayout(QtWidgets.QHBoxLayout())
        selectionWidget.layout().setContentsMargins(0, 0, 0, 0)
        self.insertWidget(0, selectionWidget)

        self.entityBrowser = _entity_browser.EntityBrowser(parent=self)
        self.entityBrowser.setMinimumSize(600, 400)
        self.entityBrowser.selectionChanged.connect(
            self._onEntityBrowserSelectionChanged)

        self.entityBrowseButton = QtWidgets.QPushButton('Browse')

        # TODO: Once the link is available through the API change this to a
        # combo with assigned tasks.
        self.assignedContextSelector = QtWidgets.QLineEdit()
        self.assignedContextSelector.setReadOnly(True)

        selectionWidget.layout().addWidget(self.assignedContextSelector)
        selectionWidget.layout().addWidget(self.entityBrowseButton)

        # Create widget used to present current selection.
        presentationWidget = QtWidgets.QFrame()
        presentationWidget.setLayout(QtWidgets.QHBoxLayout())
        presentationWidget.layout().setContentsMargins(0, 0, 0, 0)
        self.insertWidget(1, presentationWidget)

        self.entityPath = _entity_path.EntityPath()
        presentationWidget.layout().addWidget(self.entityPath)

        self.discardEntityButton = QtWidgets.QPushButton()
        removeIcon = QtGui.QIcon(QtGui.QPixmap(':/ftrack/image/light/remove'))
        self.discardEntityButton.setIconSize(QtCore.QSize(20, 20))
        self.discardEntityButton.setIcon(removeIcon)
        self.discardEntityButton.setFixedWidth(20)
        self.discardEntityButton.clicked.connect(
            self._onDiscardEntityButtonClicked)

        presentationWidget.layout().addWidget(self.discardEntityButton)

        self.entityChanged.connect(self.entityPath.setEntity)
        self.entityChanged.connect(self._updateIndex)
        self.entityBrowseButton.clicked.connect(
            self._onEntityBrowseButtonClicked)

    def _updateIndex(self, entity):
        '''Update the widget when entity changes.'''
        if entity:
            self.setCurrentIndex(1)
        else:
            self.setCurrentIndex(0)

    def _onDiscardEntityButtonClicked(self):
        '''Handle discard entity button clicked.'''
        self.setEntity(None)

    def _onEntityBrowseButtonClicked(self):
        '''Handle entity browse button clicked.'''
        # Ensure browser points to parent of currently selected entity.
        if self._entity is not None:
            location = []
            try:
                parents = self._entity.getParents()
            except AttributeError:
                pass
            else:
                for parent in parents:
                    location.append(parent.getId())

            location.reverse()
            self.entityBrowser.setLocation(location)

        # Launch browser.
        if self.entityBrowser.exec_():
            selected = self.entityBrowser.selected()
            if selected:
                # Translate new api entity to instance of ftrack.Task
                # TODO: this should not be necessary once connect has been
                # updated to use the new api.
                if selected[0].entity_type == 'Project':
                    entity = ftrack.Project(selected[0]['id'])
                else:
                    entity = ftrack.Task(selected[0]['id'])

                self.setEntity(entity)
            else:
                self.setEntity(None)

    def _onEntityBrowserSelectionChanged(self, selection):
        '''Handle selection of entity in browser.'''
        self.entityBrowser.acceptButton.setDisabled(True)
        if len(selection) == 1:
            entity = selection[0]

            if self.isValidBrowseSelection(entity):
                self.entityBrowser.acceptButton.setDisabled(False)

    def setEntity(self, entity):
        '''Set the *entity* for the view.'''
        self._entity = entity
        self.entityChanged.emit(entity)

    def getEntity(self):
        '''Return current entity.'''
        return self._entity

    def isValidBrowseSelection(self, entity):
        '''Return True if selected *entity* is valid.'''
        return True
Пример #21
0
class ConfigureScenario(QtWidgets.QWidget):
    '''Configure scenario widget.'''

    #: Signal to emit when configuration is completed.
    configuration_completed = QtCore.Signal()

    def __init__(self, session):
        '''Instantiate the configure scenario widget.'''
        super(ConfigureScenario, self).__init__()

        # Check if user has permissions to configure scenario.
        # TODO: Update this with an actual permission check once available in
        # the API.
        try:
            session.query('Setting where name is "storage_scenario" and '
                          'group is "STORAGE"').one()
            can_configure_scenario = True
        except ftrack_api.exception.NoResultFoundError:
            can_configure_scenario = False

        layout = QtWidgets.QVBoxLayout()
        layout.addSpacing(0)
        layout.setContentsMargins(50, 0, 50, 0)
        self.setLayout(layout)

        layout.addSpacing(100)

        svg_renderer = QtSvg.QSvgRenderer(':ftrack/image/default/cloud-done')
        image = QtWidgets.QImage(50, 50, QtWidgets.QImage.Format_ARGB32)

        # Set the ARGB to 0 to prevent rendering artifacts.
        image.fill(0x00000000)

        svg_renderer.render(QtGui.QPainter(image))

        icon = QtWidgets.QLabel()
        icon.setPixmap(QtGui.QPixmap.fromImage(image))
        icon.setAlignment(QtCore.Qt.AlignCenter)
        icon.setObjectName('icon-label')
        layout.addWidget(icon)

        layout.addSpacing(50)

        label = QtWidgets.QLabel()
        label.setObjectName('regular-label')
        text = ('Hi there, Connect needs to be configured so that ftrack can '
                'store and track your files for you.')

        if can_configure_scenario is False:
            text += (
                '<br><br> You do not have the required permission, please ask '
                'someone with access to system settings in ftrack to '
                'configure it before you proceed.')

        label.setText(text)
        label.setContentsMargins(0, 0, 0, 0)
        label.setAlignment(QtCore.Qt.AlignCenter)
        label.setWordWrap(True)

        # Min height is required due to issue when word wrap is True and window
        # being resized which cases text to dissapear.
        label.setMinimumHeight(120)

        label.setMinimumWidth(300)
        layout.addWidget(label, alignment=QtCore.Qt.AlignCenter)

        layout.addSpacing(20)

        configure_button = QtWidgets.QPushButton(text='Configure now')
        configure_button.setObjectName('primary')
        configure_button.clicked.connect(self._configure_storage_scenario)
        configure_button.setMinimumHeight(40)
        configure_button.setMaximumWidth(125)

        dismiss_button = QtWidgets.QPushButton(text='Do it later')
        dismiss_button.clicked.connect(self._complete_configuration)
        dismiss_button.setMinimumHeight(40)
        dismiss_button.setMaximumWidth(125)

        hbox = QtWidgets.QHBoxLayout()
        hbox.addWidget(dismiss_button)
        if can_configure_scenario:
            hbox.addSpacing(10)
            hbox.addWidget(configure_button)
        layout.addLayout(hbox)

        layout.addSpacing(20)

        label = QtWidgets.QLabel()
        label.setObjectName('lead-label')
        label.setText(
            'If you decide to do this later, some of the functionality in '
            'ftrack connect and applications started from connect may not '
            'work as expected until configured.')
        label.setAlignment(QtCore.Qt.AlignCenter)
        label.setWordWrap(True)

        # Min height is required due to issue when word wrap is True and window
        # being resized which cases text to dissapear.
        label.setMinimumHeight(100)

        label.setMinimumWidth(300)
        layout.addWidget(label, alignment=QtCore.Qt.AlignCenter)

        layout.addStretch(1)

        label = QtWidgets.QLabel()
        label.setObjectName('green-link')
        label.setText(
            '<a style="color: #1CBC90;" '
            'href="{0}/doc/using/managing_versions/storage_scenario.html"> '
            'Learn more about storage scenarios.'.format(session.server_url))
        label.setAlignment(QtCore.Qt.AlignCenter)
        label.setOpenExternalLinks(True)
        layout.addWidget(label, alignment=QtCore.Qt.AlignCenter)
        layout.addSpacing(20)

        self._subscriber_identifier = session.event_hub.subscribe(
            'topic=ftrack.storage-scenario.configure-done',
            self._complete_configuration)
        self._session = session

    def _complete_configuration(self, event=None):
        '''Finish configuration.'''
        self._session.event_hub.unsubscribe(self._subscriber_identifier)
        self.configuration_completed.emit()

    def _configure_storage_scenario(self):
        '''Open browser window and go to the configuration page.'''
        webbrowser.open_new_tab(
            '{0}/#view=configure_storage_scenario&itemId=newconfigure'.format(
                self._session.server_url))
Пример #22
0
class Component(QtWidgets.QWidget):
    '''Represent a component.'''

    nameChanged = QtCore.Signal()

    def __init__(self, componentName=None, resourceIdentifier=None,
                 parent=None):
        '''Initialise widget with initial component *value* and *parent*.'''
        super(Component, self).__init__(parent=parent)
        self.setLayout(QtWidgets.QVBoxLayout())

        self.componentNameEdit = ftrack_connect.ui.widget.line_edit.LineEdit()
        self.componentNameEdit.setPlaceholderText('Enter component name')
        self.componentNameEdit.textChanged.connect(self.nameChanged)

        self.layout().addWidget(self.componentNameEdit)

        # TODO: Add theme support.
        removeIcon = QtGui.QIcon(
            QtGui.QPixmap(':/ftrack/image/light/trash')
        )

        self.removeAction = QtWidgets.QAction(
            QtGui.QIcon(removeIcon), 'Remove', self.componentNameEdit
        )
        self.removeAction.setStatusTip('Remove component.')
        self.componentNameEdit.addAction(
            self.removeAction
        )

        self.resourceInformation = ftrack_connect.ui.widget.label.Label()
        self.layout().addWidget(self.resourceInformation)

        # Set initial values.
        self.setId(str(uuid.uuid4()))
        self.setComponentName(componentName)
        self.setResourceIdentifier(resourceIdentifier)

    def value(self):
        '''Return dictionary with component data.'''
        return {
            'id': self.id(),
            'componentName': self.componentName(),
            'resourceIdentifier': self.resourceIdentifier()
        }

    def computeComponentName(self, resourceIdentifier):
        '''Return a relevant component name using *resourceIdentifier*.'''
        name = os.path.basename(resourceIdentifier)
        if not name:
            name = resourceIdentifier
        else:
            name = name.split('.')[0]

        return name

    def id(self):
        '''Return current id.'''
        return self._id

    def setId(self, componentId):
        '''Set id to *componentId*.'''
        self._id = componentId

    def componentName(self):
        '''Return current component name.'''
        return self.componentNameEdit.text()

    def setComponentName(self, componentName):
        '''Set *componentName*.'''
        self.componentNameEdit.setText(componentName)

    def resourceIdentifier(self):
        '''Return current resource identifier.'''
        return self.resourceInformation.text()

    def setResourceIdentifier(self, resourceIdentifier):
        '''Set *resourceIdentifier*.'''
        self.resourceInformation.setText(resourceIdentifier)

        if not self.componentName():
            self.setComponentName(
                self.computeComponentName(resourceIdentifier)
            )
Пример #23
0
class ExportAssetOptionsWidget(QtWidgets.QWidget):
    clickedAssetSignal = QtCore.Signal(str)
    clickedAssetTypeSignal = QtCore.Signal(str)

    def __init__(self, parent, browseMode='Shot'):
        QtWidgets.QWidget.__init__(self, parent)
        self.ui = Ui_ExportAssetOptions()
        self.ui.setupUi(self)
        self.currentAssetType = None
        self.currentTask = None
        self.browseMode = browseMode
        self.ui.ListAssetsViewModel = QtGui.QStandardItemModel()

        self.ui.ListAssetsSortModel = QtCore.QSortFilterProxyModel()

        self.ui.ListAssetsSortModel.setDynamicSortFilter(True)
        self.ui.ListAssetsSortModel.setFilterKeyColumn(1)
        self.ui.ListAssetsSortModel.setSourceModel(self.ui.ListAssetsViewModel)

        self.ui.ListAssetNamesComboBox.setModel(self.ui.ListAssetsSortModel)

        self.ui.ListAssetsComboBoxModel = QtGui.QStandardItemModel()

        assetTypeItem = QtGui.QStandardItem('Select AssetType')
        self.assetTypes = []
        self.assetTypes.append('')
        self.ui.ListAssetsComboBoxModel.appendRow(assetTypeItem)

        assetHandler = FTAssetHandlerInstance.instance()
        self.assetTypesStr = sorted(assetHandler.getAssetTypes())

        for assetTypeStr in self.assetTypesStr:
            try:
                assetType = ftrack.AssetType(assetTypeStr)
            except:
                log.warning('{0} not available in ftrack'.format(assetTypeStr))
                continue
            assetTypeItem = QtGui.QStandardItem(assetType.getName())
            assetTypeItem.type = assetType.getShort()
            self.assetTypes.append(assetTypeItem.type)
            self.ui.ListAssetsComboBoxModel.appendRow(assetTypeItem)

        self.ui.ListAssetsComboBox.setModel(self.ui.ListAssetsComboBoxModel)

        self.ui.AssetTaskComboBoxModel = QtGui.QStandardItemModel()
        self.ui.AssetTaskComboBox.setModel(self.ui.AssetTaskComboBoxModel)

        self.ui.ListAssetNamesComboBox.currentIndexChanged[str].connect(
            self.onAssetChanged)

        if browseMode == 'Task':
            self.ui.AssetTaskComboBox.hide()
            self.ui.assetTaskLabel.hide()

    def onAssetChanged(self, asset_name):
        '''Hanldes the asset name logic on asset change'''
        if asset_name != 'New':
            self.ui.AssetNameLineEdit.setEnabled(False)
            self.ui.AssetNameLineEdit.setText(asset_name)
        else:
            self.ui.AssetNameLineEdit.setEnabled(True)
            self.ui.AssetNameLineEdit.setText('')

    @QtCore.Slot(object)
    def updateView(self, ftrackEntity):
        '''Update view with the provided *ftrackEntity*'''
        try:
            self.currentTask = ftrackEntity
            project = self.currentTask.getProject()
            taskid = '11c137c0-ee7e-4f9c-91c5-8c77cec22b2c'
            # Populate statuses based on task if it is a task.
            if self.currentTask.get('object_typeid') == taskid:
                self.ui.ListStatusComboBox.show()
                self.ui.assetTaskLabel_2.show()
                self.ui.ListStatusComboBox.clear()
                statuses = project.getTaskStatuses(
                    self.currentTask.get('typeid'))
                for index, status, in enumerate(statuses):
                    self.ui.ListStatusComboBox.addItem(status.getName())
                    if status.get('statusid') == self.currentTask.get(
                            'statusid'):
                        self.ui.ListStatusComboBox.setCurrentIndex(index)
            else:
                self.ui.ListStatusComboBox.hide()
                self.ui.assetTaskLabel_2.hide()

            if self.browseMode == 'Task':
                task = self.currentTask.getParent()

            assets = task.getAssets(assetTypes=self.assetTypesStr)
            assets = sorted(assets, key=lambda a: a.getName().lower())
            self.ui.ListAssetsViewModel.clear()

            item = QtGui.QStandardItem('New')
            item.id = ''
            curAssetType = self.currentAssetType
            if curAssetType:
                itemType = QtGui.QStandardItem(curAssetType)
            else:
                itemType = QtGui.QStandardItem('')
            self.ui.ListAssetsViewModel.setItem(0, 0, item)
            self.ui.ListAssetsViewModel.setItem(0, 1, itemType)
            self.ui.ListAssetNamesComboBox.setCurrentIndex(0)

            blankRows = 0
            for i in range(0, len(assets)):
                assetName = assets[i].getName()
                if assetName != '':
                    item = QtGui.QStandardItem(assetName)
                    item.id = assets[i].getId()
                    itemType = QtGui.QStandardItem(
                        assets[i].getType().getShort())

                    j = i - blankRows + 1
                    self.ui.ListAssetsViewModel.setItem(j, 0, item)
                    self.ui.ListAssetsViewModel.setItem(j, 1, itemType)
                else:
                    blankRows += 1
        except:
            import traceback
            import sys
            traceback.print_exc(file=sys.stdout)

    @QtCore.Slot(QtCore.QModelIndex)
    def emitAssetId(self, modelindex):
        '''Signal for emitting changes on the assetId for the give *modelIndex*'''
        clickedItem = self.ui.ListAssetsViewModel.itemFromIndex(
            self.ui.ListAssetsSortModel.mapToSource(modelindex))
        self.clickedAssetSignal.emit(clickedItem.id)

    @QtCore.Slot(int)
    def emitAssetType(self, comboIndex):
        '''Signal for emitting changes on the assetId for the give *comboIndex*'''

        comboItem = self.ui.ListAssetsComboBoxModel.item(comboIndex)
        if type(comboItem.type) is str:
            self.clickedAssetTypeSignal.emit(comboItem.type)
            self.currentAssetType = comboItem.type

    @QtCore.Slot(int)
    def setFilter(self, comboBoxIndex):
        '''Set filtering for the given *comboBoxIndex*'''
        if comboBoxIndex:
            comboItem = self.ui.ListAssetsComboBoxModel.item(comboBoxIndex)
            newItem = self.ui.ListAssetsViewModel.item(0, 1)
            newItem.setText(comboItem.type)
            self.ui.ListAssetsSortModel.setFilterFixedString(comboItem.type)
        else:
            self.ui.ListAssetsSortModel.setFilterFixedString('')

    def setAssetType(self, assetType):
        '''Set the asset to the given *assetType*'''
        for position, item in enumerate(self.assetTypes):
            if item == assetType:
                assetTypeIndex = int(position)
                if assetTypeIndex == self.ui.ListAssetsComboBox.currentIndex():
                    self.ui.ListAssetsComboBox.setCurrentIndex(0)
                self.ui.ListAssetsComboBox.setCurrentIndex(assetTypeIndex)

    def setAssetName(self, assetName):
        '''Set the asset to the given *assetName*'''
        self.ui.AssetNameLineEdit.setText('')
        rows = self.ui.ListAssetsSortModel.rowCount()
        existingAssetFound = False
        for i in range(rows):
            index = self.ui.ListAssetsSortModel.index(i, 0)
            datas = self.ui.ListAssetsSortModel.data(index)

            if datas == assetName:
                self.ui.ListAssetNamesComboBox.setCurrentIndex(int(i))
                existingAssetFound = True

        if not existingAssetFound:
            self.ui.AssetNameLineEdit.setText(assetName)

    def getAssetType(self):
        '''Return the current asset type'''
        return self.currentAssetType

    @QtCore.Slot(object)
    def updateTasks(self, ftrackEntity):
        '''Update task with the provided *ftrackEntity*'''
        self.currentTask = ftrackEntity
        try:
            shotpath = self.currentTask.getName()
            taskParents = self.currentTask.getParents()

            for parent in taskParents:
                shotpath = '{0}.{1}'.format(parent.getName(), shotpath)

            self.ui.AssetTaskComboBox.clear()
            tasks = self.currentTask.getTasks()
            curIndex = 0
            ftrackuser = ftrack.User(getpass.getuser())
            taskids = [x.getId() for x in ftrackuser.getTasks()]

            for i in range(len(tasks)):
                assetTaskItem = QtGui.QStandardItem(tasks[i].getName())
                assetTaskItem.id = tasks[i].getId()
                self.ui.AssetTaskComboBoxModel.appendRow(assetTaskItem)

                if (os.environ.get('FTRACK_TASKID') == assetTaskItem.id):
                    curIndex = i
                else:
                    if assetTaskItem.id in taskids:
                        curIndex = i

            self.ui.AssetTaskComboBox.setCurrentIndex(curIndex)

        except:
            print 'Not a task'

    def getShot(self):
        '''Return the current shot'''
        if self.browseMode == 'Shot':
            return self.currentTask
        else:
            return self.currentTask.getParent()

    def getTask(self):
        '''Return the current task'''
        if self.browseMode == 'Shot':
            comboItem = self.ui.AssetTaskComboBoxModel.item(
                self.ui.AssetTaskComboBox.currentIndex())
            if comboItem:
                return ftrack.Task(comboItem.id)
            else:
                return None
        else:
            return self.currentTask

    def getStatus(self):
        '''Return the current asset status'''
        return self.ui.ListStatusComboBox.currentText()

    def getAssetName(self):
        '''Retain logic for defining a new asset name'''
        if self.ui.ListAssetNamesComboBox.currentText() == 'New':
            return self.ui.AssetNameLineEdit.text()
        else:
            return self.ui.ListAssetNamesComboBox.currentText()
Пример #24
0
class CreateAssetTypeOverlay(Overlay):
    '''Create asset type overlay.'''

    asset_creation_failed = QtCore.Signal()

    def __init__(self, session, parent):
        '''Instantiate with *session*.'''
        super(CreateAssetTypeOverlay, self).__init__(parent=parent)
        self.session = session

        self.main_layout = QtWidgets.QVBoxLayout()
        self.setLayout(self.main_layout)

        icon = QtGui.QPixmap(':ftrack/image/default/ftrackLogoColor')
        icon = icon.scaled(QtCore.QSize(85, 85), QtCore.Qt.KeepAspectRatio,
                           QtCore.Qt.SmoothTransformation)
        self.ftrack_icon = QtWidgets.QLabel()
        self.ftrack_icon.setPixmap(icon)

        self.main_layout.addStretch(1)
        self.main_layout.insertWidget(1,
                                      self.ftrack_icon,
                                      alignment=QtCore.Qt.AlignCenter)
        self.main_layout.addStretch(1)
        self.main_layout.setContentsMargins(20, 20, 20, 20)

        # create asset type widget
        self.create_asset_widget = QtWidgets.QFrame()
        self.create_asset_widget.setVisible(False)

        create_asset_layout = QtWidgets.QVBoxLayout()
        create_asset_layout.setContentsMargins(20, 20, 20, 20)
        create_asset_layout.addStretch(1)
        buttons_layout = QtWidgets.QHBoxLayout()
        self.create_asset_widget.setLayout(create_asset_layout)

        self.create_asset_label_top = QtWidgets.QLabel()

        self.create_asset_label_bottom = QtWidgets.QLabel(
            '<h4>Do you want to create one ?</h4>')

        create_asset_layout.insertWidget(1,
                                         self.create_asset_label_top,
                                         alignment=QtCore.Qt.AlignCenter)
        create_asset_layout.insertWidget(2,
                                         self.create_asset_label_bottom,
                                         alignment=QtCore.Qt.AlignCenter)
        self.create_asset_button = QtWidgets.QPushButton('Create')
        self.cancel_asset_button = QtWidgets.QPushButton('Cancel')
        create_asset_layout.addLayout(buttons_layout)
        buttons_layout.addWidget(self.create_asset_button)
        buttons_layout.addWidget(self.cancel_asset_button)

        # result create asset type
        self.create_asset_widget_result = QtWidgets.QFrame()
        self.create_asset_widget_result.setVisible(False)

        create_asset_layout_result = QtWidgets.QVBoxLayout()
        create_asset_layout_result.setContentsMargins(20, 20, 20, 20)
        create_asset_layout_result.addStretch(1)

        self.create_asset_widget_result.setLayout(create_asset_layout_result)
        self.create_asset_label_result = QtWidgets.QLabel()
        self.continue_button = QtWidgets.QPushButton('Continue')

        create_asset_layout_result.insertWidget(
            1, self.create_asset_label_result, alignment=QtCore.Qt.AlignCenter)

        create_asset_layout_result.insertWidget(
            2, self.continue_button, alignment=QtCore.Qt.AlignCenter)

        # error on create asset
        self.create_asset_widget_error = QtWidgets.QFrame()
        self.create_asset_widget_error.setVisible(False)

        create_asset_layout_error = QtWidgets.QVBoxLayout()
        create_asset_layout_error.setContentsMargins(20, 20, 20, 20)
        create_asset_layout_error.addStretch(1)

        self.create_asset_widget_error.setLayout(create_asset_layout_error)
        self.create_asset_label_error = QtWidgets.QLabel()
        self.close_button = QtWidgets.QPushButton('Close')

        create_asset_layout_error.insertWidget(1,
                                               self.create_asset_label_error,
                                               alignment=QtCore.Qt.AlignCenter)

        create_asset_layout_error.insertWidget(2,
                                               self.close_button,
                                               alignment=QtCore.Qt.AlignCenter)

        # parent all.
        self.main_layout.addWidget(self.create_asset_widget)
        self.main_layout.addWidget(self.create_asset_widget_result)
        self.main_layout.addWidget(self.create_asset_widget_error)

        self.main_layout.addStretch(1)

        # signals
        self.create_asset_button.clicked.connect(self.on_create_asset)
        self.continue_button.clicked.connect(self.on_continue)
        self.close_button.clicked.connect(self.on_fail)
        self.cancel_asset_button.clicked.connect(self.on_fail)

    def populate(self, asset_type_short, asset_type):
        '''Populate with *asset_type_short* and *asset_type*.'''
        self.create_asset_widget.setVisible(True)
        self.create_asset_widget_result.setVisible(False)
        self.create_asset_widget_error.setVisible(False)

        self.asset_type = asset_type
        self.asset_type_short = asset_type_short

        self.create_asset_label_top.setText(
            '<h2>The required asset type {0} ({1}) does not exist on your '
            'ftrack instance.</h2>'.format(self.asset_type,
                                           self.asset_type_short))

    def on_fail(self):
        '''Handle fail.'''
        self.asset_creation_failed.emit()
        self.setVisible(False)

    def on_continue(self):
        '''Handle continue.'''
        self.setVisible(False)

    def on_create_asset(self):
        '''Handle asset created.'''
        result = ftrack_connect_pipeline.util.create_asset_type(
            self.session, self.asset_type, self.asset_type_short)

        if result['status']:
            self.create_asset_widget.setVisible(False)
            self.create_asset_label_result.setText(
                '<center><h2>{0}</h2></center>'.format(result['message']))
            self.create_asset_widget_result.setVisible(True)
        else:
            self.create_asset_widget.setVisible(False)
            self.create_asset_label_error.setText(
                '<center><h2>{0}</h2></center>'.format(result['message']))
            self.create_asset_widget_error.setVisible(True)
Пример #25
0
class ActionItem(QtWidgets.QWidget):
    '''Widget representing an action item.'''

    #: Emitted before an action is launched with action
    beforeActionLaunch = QtCore.Signal(dict, name='beforeActionLaunch')

    #: Emitted after an action has been launched with action and results
    actionLaunched = QtCore.Signal(dict, list)

    def __init__(self, actions, parent=None):
        '''Initialize action item with *actions*

        *actions* should be a list of action dictionaries with the same label.
        Each action may contain a the following:

        label
            To be displayed as text
        icon
            An URL to an image or one of the provided icons.
        variant
            A variant of the action. Will be shown in the menu shown for 
            multiple actions, or as part of the label for a single action.
        description
            A optional description of the action to be shown on hover.

        Label, icon and description will be retrieved from the first action if
        multiple actions are specified.
        '''
        super(ActionItem, self).__init__(parent=parent)
        self.logger = logging.getLogger(__name__ + '.' +
                                        self.__class__.__name__)

        self.setMouseTracking(True)
        self.setFixedSize(QtCore.QSize(80, 80))
        layout = QtWidgets.QVBoxLayout()
        layout.setAlignment(QtCore.Qt.AlignCenter)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

        if not actions:
            raise ValueError('No actions specified')

        self._actions = actions
        self._label = actions[0].get('label', 'Untitled action')
        self._icon = actions[0].get('icon', None)
        self._description = actions[0].get('description', None)
        self._variants = [
            u'{0} {1}'.format(action.get('label', 'Untitled action'),
                              action.get('variant', '')).strip()
            for action in actions
        ]

        if len(actions) == 1:
            if actions[0].get('variant'):
                self._label = u'{0} {1}'.format(self._label,
                                                actions[0].get('variant'))

            self._hoverIcon = 'play'
            self._multiple = False
        else:
            self._hoverIcon = 'menu'
            self._multiple = True

        self._iconLabel = ActionIcon(self)
        self._iconLabel.setAlignment(QtCore.Qt.AlignCenter)
        self._iconLabel.setFixedSize(QtCore.QSize(80, 45))
        layout.addWidget(self._iconLabel)

        self._textLabel = QtWidgets.QLabel(self)
        self._textLabel.setAlignment(QtCore.Qt.AlignHCenter
                                     | QtCore.Qt.AlignTop)
        self._textLabel.setWordWrap(True)
        self._textLabel.setFixedSize(QtCore.QSize(80, 35))
        layout.addWidget(self._textLabel)

        self.setText(self._label)
        self.setIcon(self._icon)
        if self._description:
            self.setToolTip(self._description)

    def mouseReleaseEvent(self, event):
        '''Launch action on mouse release. 

        First show menu with variants if multiple actions are available.
        '''
        if self._multiple:
            self.logger.debug('Launching menu to select action variant')
            menu = QtWidgets.QMenu(self)
            for index, variant in enumerate(self._variants):
                action = QtWidgets.QAction(variant, self)
                action.setData(index)
                menu.addAction(action)

            result = menu.exec_(QtWidgets.QCursor.pos())
            if result is None:
                return

            action = self._actions[result.data()]
        else:
            action = self._actions[0]

        self._launchAction(action)

    def enterEvent(self, event):
        '''Show hover icon on mouse enter.'''
        self._iconLabel.loadResource('{0}{1}'.format(':/ftrack/image/light/',
                                                     self._hoverIcon))

    def leaveEvent(self, event):
        '''Reset action icon on mouse leave.'''
        self.setIcon(self._icon)

    def setText(self, text):
        '''Set *text* on text label.'''
        self._textLabel.setText(text)

    def setIcon(self, icon):
        '''Set icon on icon label.'''
        self._iconLabel.setIcon(icon)

    def _launchAction(self, action):
        '''Launch *action* via event hub.'''
        self.logger.info(u'Launching action: {0}'.format(action))
        self.beforeActionLaunch.emit(action)
        self._publishLaunchActionEvent(action)

    @ftrack_connect.asynchronous.asynchronous
    def _publishLaunchActionEvent(self, action):
        '''Launch *action* asynchronously and emit *actionLaunched* when completed.'''
        try:
            if action.is_new_api:
                session = ftrack_connect.session.get_shared_session()
                results = session.event_hub.publish(
                    ftrack_api.event.base.Event(topic='ftrack.action.launch',
                                                data=action),
                    synchronous=True)
            else:
                results = ftrack.EVENT_HUB.publish(ftrack.Event(
                    topic='ftrack.action.launch', data=action),
                                                   synchronous=True)

        except Exception as error:
            results = [{
                'success': False,
                'message': 'Failed to launch action'
            }]
            self.logger.warning(
                u'Action launch failed with exception: {0}'.format(error))

        self.logger.debug('Launched action with result: {0}'.format(results))
        self.actionLaunched.emit(action, results)
Пример #26
0
class Login(QtWidgets.QWidget):
    '''Login widget class.'''
    # Login signal with params url, username and API key.
    login = QtCore.Signal(object, object, object)

    # Error signal that can be used to present an error message.
    loginError = QtCore.Signal(object)

    def __init__(self, *args, **kwargs):
        '''Instantiate the login widget.'''
        super(Login, self).__init__(*args, **kwargs)

        layout = QtWidgets.QVBoxLayout()
        layout.addSpacing(100)
        layout.setContentsMargins(50, 0, 50, 0)
        layout.setSpacing(15)
        self.setLayout(layout)

        label = QtWidgets.QLabel()
        label.setText('Sign in')
        label.setObjectName('login-label')
        layout.addWidget(label, alignment=QtCore.Qt.AlignCenter)

        self.server = QtWidgets.QLineEdit()
        self.server.setPlaceholderText('Site name or custom domain URL')
        layout.addWidget(self.server)

        self.username = QtWidgets.QLineEdit()
        self.username.setPlaceholderText('User name')
        self.username.hide()
        layout.addWidget(self.username)

        self.apiKey = QtWidgets.QLineEdit()
        self.apiKey.setPlaceholderText('API key')
        self.apiKey.hide()
        layout.addWidget(self.apiKey)

        loginButton = QtWidgets.QPushButton(text='SIGN IN')
        loginButton.setObjectName('primary')
        loginButton.clicked.connect(self.handleLogin)
        loginButton.setMinimumHeight(35)
        layout.addWidget(loginButton)

        label = QtWidgets.QLabel()
        label.setObjectName('lead-label')
        label.setContentsMargins(0, 0, 0, 0)
        label.setText(
            'Your site name is your ftrackapp.com web address '
            '(e.g https://sitename.ftrackapp.com OR your custom domain URL).')
        label.setAlignment(QtCore.Qt.AlignCenter)
        label.setWordWrap(True)

        # Min height is required due to issue when word wrap is True and window
        # being resized which cases text to dissapear.
        label.setMinimumHeight(50)

        label.setMinimumWidth(300)
        layout.addWidget(label, alignment=QtCore.Qt.AlignCenter)

        self.errorLabel = QtWidgets.QLabel()
        self.errorLabel.setWordWrap(True)
        layout.addWidget(self.errorLabel)
        self.loginError.connect(self.on_set_error)

        layout.addStretch(1)

        self.toggle_api_label = label = ClickableLabel()
        self.toggle_api_label.setObjectName('lead-label')
        self.toggle_api_label.setText(
            'Trouble signing in? '
            '<a href="#" style="color: #1CBC90;">Sign in with username and API key</a>'
        )
        self.toggle_api_label.clicked.connect(self._toggle_credentials)
        layout.addWidget(self.toggle_api_label,
                         alignment=QtCore.Qt.AlignCenter)
        layout.addSpacing(20)

    def on_set_error(self, error):
        '''Set the error text and disable the login widget.'''
        self.errorLabel.setText(error)

    def handleLogin(self):
        '''Fetch login data from form fields and emit login event.'''
        serverUrl = self.server.text()
        username = self.username.text()
        apiKey = self.apiKey.text()

        self.login.emit(serverUrl, username, apiKey)

    def _toggle_credentials(self):
        '''Toggle credential fields.'''
        self.apiKey.show()
        self.username.show()
        self.toggle_api_label.hide()
Пример #27
0
class EntityBrowser(QtWidgets.QDialog):
    '''Entity browser.'''

    #: Signal when location changed.
    locationChanged = QtCore.Signal()

    #: Signal when selection changes. Pass new selection.
    selectionChanged = QtCore.Signal(object)

    def __init__(self, root=None, parent=None):
        '''Initialise browser with *root* entity.

        Use an empty *root* to start with list of projects.

        *parent* is the optional owner of this UI element.

        '''
        super(EntityBrowser, self).__init__(parent=parent)
        self._root = root
        self._selected = []
        self._updatingNavigationBar = False

        self._session = util.get_session()

        self._construct()
        self._postConstruction()

    def _construct(self):
        '''Construct widget.'''
        self.setLayout(QtWidgets.QVBoxLayout())

        self.headerLayout = QtWidgets.QHBoxLayout()

        self.navigationBar = QtWidgets.QTabBar()
        self.navigationBar.setExpanding(False)
        self.navigationBar.setDrawBase(False)
        self.headerLayout.addWidget(self.navigationBar, stretch=1)

        self.navigateUpButton = QtWidgets.QToolButton()
        self.navigateUpButton.setObjectName('entity-browser-up-button')
        self.navigateUpButton.setIcon(
            QtGui.QIcon(':ftrack/image/light/upArrow')
        )
        self.navigateUpButton.setToolTip('Navigate up a level.')
        self.headerLayout.addWidget(self.navigateUpButton)

        self.reloadButton = QtWidgets.QToolButton()
        self.reloadButton.setObjectName('entity-browser-reload-button')

        self.reloadButton.setIcon(
            QtGui.QIcon(':ftrack/image/light/reload')
        )
        self.reloadButton.setToolTip('Reload listing from server.')
        self.headerLayout.addWidget(self.reloadButton)

        self.layout().addLayout(self.headerLayout)

        self.contentSplitter = QtWidgets.QSplitter()

        self.bookmarksList = QtWidgets.QListView()
        self.contentSplitter.addWidget(self.bookmarksList)

        self.view = QtWidgets.QTableView()
        self.view.setSelectionBehavior(self.view.SelectRows)
        self.view.setSelectionMode(self.view.SingleSelection)
        self.view.verticalHeader().hide()

        self.contentSplitter.addWidget(self.view)

        proxy = ftrack_connect_pipeline.ui.model.entity_tree.EntityTreeProxyModel(self)
        model = ftrack_connect_pipeline.ui.model.entity_tree.EntityTreeModel(
            root=ftrack_connect_pipeline.ui.model.entity_tree.ItemFactory(
                self._session, self._root
            ),
            parent=self
        )
        proxy.setSourceModel(model)
        proxy.setDynamicSortFilter(True)

        self.view.setModel(proxy)
        self.view.setSortingEnabled(True)

        self.contentSplitter.setStretchFactor(1, 1)
        self.layout().addWidget(self.contentSplitter)

        self.footerLayout = QtWidgets.QHBoxLayout()
        self.footerLayout.addStretch(1)

        self.cancelButton = QtWidgets.QPushButton('Cancel')
        self.footerLayout.addWidget(self.cancelButton)

        self.acceptButton = QtWidgets.QPushButton('Choose')
        self.footerLayout.addWidget(self.acceptButton)

        self.layout().addLayout(self.footerLayout)

        self.overlay = ftrack_connect_pipeline.ui.widget.overlay.BusyOverlay(
            self.view, message='Loading'
        )

    def _postConstruction(self):
        '''Perform post-construction operations.'''
        self.setWindowTitle('ftrack browser')
        self.view.sortByColumn(0, QtCore.Qt.AscendingOrder)

        # TODO: Remove once bookmarks widget implemented.
        self.bookmarksList.hide()

        self.acceptButton.setDefault(True)
        self.acceptButton.setDisabled(True)

        self.model.sourceModel().loadStarted.connect(self._onLoadStarted)
        self.model.sourceModel().loadEnded.connect(self._onLoadEnded)

        # Compatibility layer for PySide2/Qt5.
        # Please see: https://github.com/mottosso/Qt.py/issues/72
        # for more information.
        try:
            self.view.horizontalHeader().setResizeMode(
                QtWidgets.QHeaderView.ResizeToContents
            )
            self.view.horizontalHeader().setResizeMode(
                0, QtWidgets.QHeaderView.Stretch
            )
        except Exception:
            self.view.horizontalHeader().setSectionResizeMode(
                QtWidgets.QHeaderView.ResizeToContents
            )
            self.view.horizontalHeader().setSectionResizeMode(
                0, QtWidgets.QHeaderView.Stretch
            )

        self.acceptButton.clicked.connect(self.accept)
        self.cancelButton.clicked.connect(self.reject)

        self.navigationBar.currentChanged.connect(
            self._onSelectNavigationBarItem
        )
        self.navigateUpButton.clicked.connect(self._onNavigateUpButtonClicked)
        self.reloadButton.clicked.connect(self._onReloadButtonClicked)
        self.view.activated.connect(self._onActivateItem)

        selectionModel = self.view.selectionModel()
        selectionModel.selectionChanged.connect(self._onSelectionChanged)

        self._updateNavigationBar()

    @property
    def model(self):
        '''Return current model.'''
        return self.view.model()

    def selected(self):
        '''Return selected entities.'''
        return self._selected[:]

    def setLocation(self, location):
        '''Set location to *location*.

        *location* should be a list of entries representing the 'path' from the
        root of the model to the desired location.

        Each entry in the list should be an entity id.

        '''
        # Ensure root children loaded in order to begin search.
        rootIndex = self.model.index(-1, -1)
        if (
            self.model.hasChildren(rootIndex)
            and self.model.canFetchMore(rootIndex)
        ):
            self.model.fetchMore(rootIndex)

        # Search for matching entries by identity.
        role = self.model.sourceModel().IDENTITY_ROLE

        matchingIndex = rootIndex
        searchIndex = self.model.index(0, 0)
        for identity in location:
            matches = self.model.match(
                searchIndex, role, identity
            )
            if not matches:
                break

            matchingIndex = matches[0]
            if (
                self.model.hasChildren(matchingIndex)
                and self.model.canFetchMore(matchingIndex)
            ):
                self.model.fetchMore(matchingIndex)

            searchIndex = self.model.index(0, 0, parent=matchingIndex)

        else:
            self.setLocationFromIndex(matchingIndex)
            return

        raise ValueError('Could not match location {0!r}'.format(location))

    def getLocation(self):
        '''Return current location as list of entity ids from root.'''
        location = []
        item = self.model.item(self.view.rootIndex())
        while item is not None and item.entity != self._root:
            location.append(item.id)
            item = item.parent

        location.reverse()
        return location

    def setLocationFromIndex(self, index):
        '''Set location to *index*.'''
        if index is None:
            index = QtCore.QModelIndex()

        currentIndex = self.view.rootIndex()
        if index == currentIndex:
            return

        self.view.setRootIndex(index)
        self._updateNavigationBar()

        selectionModel = self.view.selectionModel()
        selectionModel.clearSelection()

        self.locationChanged.emit()

    def _onLoadStarted(self):
        '''Handle load started.'''
        self.reloadButton.setEnabled(False)
        self.overlay.show()

    def _onLoadEnded(self):
        '''Handle load ended.'''
        self.overlay.hide()
        self.reloadButton.setEnabled(True)

    def _updateNavigationBar(self):
        '''Update navigation bar.'''
        if self._updatingNavigationBar:
            return

        self._updatingNavigationBar = True

        # Clear all existing entries.
        for index in range(self.navigationBar.count(), -1, -1):
            self.navigationBar.removeTab(index)

        # Compute new entries.
        entries = []
        index = self.view.rootIndex()
        while index.isValid():
            item = self.model.item(index)
            entries.append(
                dict(icon=item.icon, label=item.name, index=index)
            )
            index = self.model.parent(index)

        item = self.model.root
        entries.append(
            dict(icon=item.icon, label=item.name, index=None)
        )

        entries.reverse()
        for entry in entries:
            tabIndex = self.navigationBar.addTab(entry['icon'], entry['label'])
            self.navigationBar.setTabData(tabIndex, entry['index'])
            self.navigationBar.setCurrentIndex(tabIndex)

        self._updatingNavigationBar = False

    def _onSelectNavigationBarItem(self, index):
        '''Handle selection of navigation bar item.'''
        if index < 0:
            return

        if self._updatingNavigationBar:
            return

        modelIndex = self.navigationBar.tabData(index)
        self.setLocationFromIndex(modelIndex)

    def _onActivateItem(self, index):
        '''Handle activation of item in listing.'''
        if self.model.hasChildren(index):
            self.setLocationFromIndex(index)

    def _onSelectionChanged(self, selected, deselected):
        '''Handle change of *selection*.'''
        del self._selected[:]
        seen = set()

        for index in selected.indexes():
            row = index.row()
            if row in seen:
                continue

            seen.add(row)

            item = self.model.item(index)
            if item:
                self._selected.append(item.entity)

        selected = self.selected()
        if selected:
            self.acceptButton.setEnabled(True)
        else:
            self.acceptButton.setEnabled(False)

        self.selectionChanged.emit(self.selected())

    def _onNavigateUpButtonClicked(self):
        '''Navigate up on button click.'''
        currentRootIndex = self.view.rootIndex()
        parent = self.model.parent(currentRootIndex)
        self.setLocationFromIndex(parent)

    def _onReloadButtonClicked(self):
        '''Reload current index on button click.'''
        currentRootIndex = self.view.rootIndex()
        self.model.reloadChildren(currentRootIndex)
Пример #28
0
class Application(QtWidgets.QMainWindow):
    '''Main application window for ftrack connect.'''

    #: Signal when login fails.
    loginError = QtCore.Signal(object)

    #: Signal when event received via ftrack's event hub.
    eventHubSignal = QtCore.Signal(object)

    # Login signal.
    loginSignal = QtCore.Signal(object, object, object)

    def __init__(self, *args, **kwargs):
        '''Initialise the main application window.'''
        theme = kwargs.pop('theme', 'light')
        super(Application, self).__init__(*args, **kwargs)
        self.logger = logging.getLogger(
            __name__ + '.' + self.__class__.__name__
        )

        self.defaultPluginDirectory = appdirs.user_data_dir(
            'ftrack-connect-plugins', 'ftrack'
        )

        self.pluginHookPaths = set()
        self.pluginHookPaths.update(
            self._gatherPluginHooks(
                self.defaultPluginDirectory
            )
        )
        if 'FTRACK_CONNECT_PLUGIN_PATH' in os.environ:
            for connectPluginPath in (
                os.environ['FTRACK_CONNECT_PLUGIN_PATH'].split(os.pathsep)
            ):
                self.pluginHookPaths.update(
                    self._gatherPluginHooks(
                        connectPluginPath
                    )
                )

        self.logger.info(
            u'Connect plugin hooks directories: {0}'.format(
                ', '.join(self.pluginHookPaths)
            )
        )

        # Register widget for error handling.
        self.uncaughtError = _uncaught_error.UncaughtError(
            parent=self
        )

        if not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable():
            raise ftrack_connect.error.ConnectError(
                'No system tray located.'
            )

        self.logoIcon = QtGui.QIcon(
            QtGui.QPixmap(':/ftrack/image/default/ftrackLogoColor')
        )

        self._login_server_thread = None

        self._theme = None
        self.setTheme(theme)

        self.plugins = {}

        self._initialiseTray()

        self.setObjectName('ftrack-connect-window')
        self.setWindowTitle('ftrack connect')
        self.resize(450, 700)
        self.move(50, 50)

        self.setWindowIcon(self.logoIcon)

        self._login_overlay = None
        self.loginWidget = _login.Login()
        self.loginSignal.connect(self.loginWithCredentials)
        self.login()

    def theme(self):
        '''Return current theme.'''
        return self._theme

    def setTheme(self, theme):
        '''Set *theme*.'''
        self._theme = theme
        ftrack_connect.ui.theme.applyFont()
        ftrack_connect.ui.theme.applyTheme(self, self._theme, 'cleanlooks')

    def _onConnectTopicEvent(self, event):
        '''Generic callback for all ftrack.connect events.

        .. note::
            Events not triggered by the current logged in user will be dropped.

        '''
        if event['topic'] != 'ftrack.connect':
            return

        self._routeEvent(event)

    def logout(self):
        '''Clear stored credentials and quit Connect.'''
        self._clear_qsettings()
        config = ftrack_connect.ui.config.read_json_config()

        config['accounts'] = []
        ftrack_connect.ui.config.write_json_config(config)

        QtWidgets.qApp.quit()

    def _clear_qsettings(self):
        '''Remove credentials from QSettings.'''
        settings = QtCore.QSettings()
        settings.remove('login')

    def _get_credentials(self):
        '''Return a dict with API credentials from storage.'''
        credentials = None

        # Read from json config file.
        json_config = ftrack_connect.ui.config.read_json_config()
        if json_config:
            try:
                data = json_config['accounts'][0]
                credentials = {
                    'server_url': data['server_url'],
                    'api_user': data['api_user'],
                    'api_key': data['api_key']
                }
            except Exception:
                self.logger.debug(
                    u'No credentials were found in config: {0}.'.format(
                        json_config
                    )
                )

        # Fallback on old QSettings.
        if not json_config and not credentials:
            settings = QtCore.QSettings()
            server_url = settings.value('login/server', None)
            api_user = settings.value('login/username', None)
            api_key = settings.value('login/apikey', None)

            if not None in (server_url, api_user, api_key):
                credentials = {
                    'server_url': server_url,
                    'api_user': api_user,
                    'api_key': api_key
                }

        return credentials

    def _save_credentials(self, server_url, api_user, api_key):
        '''Save API credentials to storage.'''
        # Clear QSettings since they should not be used any more.
        self._clear_qsettings()

        # Save the credentials.
        json_config = ftrack_connect.ui.config.read_json_config()

        if not json_config:
            json_config = {}

        # Add a unique id to the config that can be used to identify this
        # machine.
        if not 'id' in json_config:
            json_config['id'] = str(uuid.uuid4())

        json_config['accounts'] = [{
            'server_url': server_url,
            'api_user': api_user,
            'api_key': api_key
        }]

        ftrack_connect.ui.config.write_json_config(json_config)

    def login(self):
        '''Login using stored credentials or ask user for them.'''
        credentials = self._get_credentials()
        self.showLoginWidget()

        if credentials:
            # Try to login.
            self.loginWithCredentials(
                credentials['server_url'],
                credentials['api_user'],
                credentials['api_key']
            )

    def showLoginWidget(self):
        '''Show the login widget.'''
        self._login_overlay = ftrack_connect.ui.widget.overlay.CancelOverlay(
            self.loginWidget,
            message='Signing in'
        )

        self._login_overlay.hide()
        self.setCentralWidget(self.loginWidget)
        self.loginWidget.login.connect(self._login_overlay.show)
        self.loginWidget.login.connect(self.loginWithCredentials)
        self.loginError.connect(self.loginWidget.loginError.emit)
        self.loginError.connect(self._login_overlay.hide)
        self.focus()

        # Set focus on the login widget to remove any focus from its child
        # widgets.
        self.loginWidget.setFocus()
        self._login_overlay.hide()

    def _setup_session(self):
        '''Setup a new python API session.'''
        if hasattr(self, '_hub_thread'):
            self._hub_thread.quit()

        plugin_paths = os.environ.get(
            'FTRACK_EVENT_PLUGIN_PATH', ''
        ).split(os.pathsep)

        plugin_paths.extend(self.pluginHookPaths)

        try:
            session = ftrack_connect.session.get_shared_session(
                plugin_paths=plugin_paths
            )
        except Exception as error:
            raise ftrack_connect.error.ParseError(error)

        # Listen to events using the new API event hub. This is required to
        # allow reconfiguring the storage scenario.
        self._hub_thread = _event_hub_thread.NewApiEventHubThread()
        self._hub_thread.start(session)

        ftrack_api._centralized_storage_scenario.register_configuration(
            session
        )

        return session

    def _report_session_setup_error(self, error):
        '''Format error message and emit loginError.'''
        msg = (
            u'\nAn error occured while starting ftrack-connect: <b>{0}</b>.'
            u'\nPlease check log file for more informations.'
            u'\nIf the error persists please send the log file to:'
            u' [email protected]'.format(error)

        )
        self.loginError.emit(msg)

    def loginWithCredentials(self, url, username, apiKey):
        '''Connect to *url* with *username* and *apiKey*.

        loginError will be emitted if this fails.

        '''
        # Strip all leading and preceeding occurances of slash and space.
        url = url.strip('/ ')

        if not url:
            self.loginError.emit(
                'You need to specify a valid server URL, '
                'for example https://server-name.ftrackapp.com'
            )
            return

        if not 'http' in url:
            if url.endswith('ftrackapp.com'):
                url = 'https://' + url
            else:
                url = 'https://{0}.ftrackapp.com'.format(url)

        try:
            result = requests.get(
                url,
                allow_redirects=False  # Old python API will not work with redirect.
            )
        except requests.exceptions.RequestException:
            self.logger.exception('Error reaching server url.')
            self.loginError.emit(
                'The server URL you provided could not be reached.'
            )
            return

        if (
            result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
        ):
            self.loginError.emit(
                'The server URL you provided is not a valid ftrack server.'
            )
            return

        # If there is an existing server thread running we need to stop it.
        if self._login_server_thread:
            self._login_server_thread.quit()
            self._login_server_thread = None

        # If credentials are not properly set, try to get them using a http
        # server.
        if not username or not apiKey:
            self._login_server_thread = _login_tools.LoginServerThread()
            self._login_server_thread.loginSignal.connect(self.loginSignal)
            self._login_server_thread.start(url)
            return

        # Set environment variables supported by the old API.
        os.environ['FTRACK_SERVER'] = url
        os.environ['LOGNAME'] = username
        os.environ['FTRACK_APIKEY'] = apiKey

        # Set environment variables supported by the new API.
        os.environ['FTRACK_API_USER'] = username
        os.environ['FTRACK_API_KEY'] = apiKey

        # Login using the new ftrack API.
        try:
            self._session = self._setup_session()
        except Exception as error:
            self.logger.exception(u'Error during login.:')
            self._report_session_setup_error(error)
            return

        # Store credentials since login was successful.
        self._save_credentials(url, username, apiKey)

        # Verify storage scenario before starting.
        if 'storage_scenario' in self._session.server_information:
            storage_scenario = self._session.server_information.get(
                'storage_scenario'
            )
            if storage_scenario is None:
                # Hide login overlay at this time since it will be deleted
                self.logger.debug('Storage scenario is not configured.')
                scenario_widget = _scenario_widget.ConfigureScenario(
                    self._session
                )
                scenario_widget.configuration_completed.connect(
                    self.location_configuration_finished
                )
                self.setCentralWidget(scenario_widget)
                self.focus()
                return

        self.location_configuration_finished(reconfigure_session=False)

    def location_configuration_finished(self, reconfigure_session=True):
        '''Continue connect setup after location configuration is done.'''
        if reconfigure_session:
            ftrack_connect.session.destroy_shared_session()
            self._session = self._setup_session()

        try:
            self.configureConnectAndDiscoverPlugins()
        except Exception as error:
            self.logger.exception(u'Error during location configuration.:')
            self._report_session_setup_error(error)
        else:
            self.focus()

        # Send verify startup event to verify that storage scenario is
        # working correctly.
        event = ftrack_api.event.base.Event(
            topic='ftrack.connect.verify-startup',
            data={
                'storage_scenario': self._session.server_information.get(
                    'storage_scenario'
                )
            }
        )
        results = self._session.event_hub.publish(event, synchronous=True)
        problems = [
            problem for problem in results if isinstance(problem, basestring)
        ]
        if problems:
            msgBox = QtWidgets.QMessageBox(parent=self)
            msgBox.setIcon(QtWidgets.QMessageBox.Warning)
            msgBox.setText('\n\n'.join(problems))
            msgBox.exec_()

    def configureConnectAndDiscoverPlugins(self):
        '''Configure connect and load plugins.'''

        # Local import to avoid connection errors.
        import ftrack
        ftrack.EVENT_HANDLERS.paths.extend(self.pluginHookPaths)
        ftrack.LOCATION_PLUGINS.paths.extend(self.pluginHookPaths)

        ftrack.setup()
        self.tabPanel = _tab_widget.TabWidget()
        self.tabPanel.tabBar().setObjectName('application-tab-bar')
        self.setCentralWidget(self.tabPanel)

        self._discoverTabPlugins()

        ftrack.EVENT_HUB.subscribe(
            'topic=ftrack.connect and source.user.username={0}'.format(
                getpass.getuser()
            ),
            self._relayEventHubEvent
        )
        self.eventHubSignal.connect(self._onConnectTopicEvent)

        self.eventHubThread = _event_hub_thread.EventHubThread()
        self.eventHubThread.start()

        self.focus()

        # Listen to discover connect event and respond to let the sender know
        # that connect is running.
        ftrack.EVENT_HUB.subscribe(
            'topic=ftrack.connect.discover and source.user.username={0}'.format(
                getpass.getuser()
            ),
            lambda event : True
        )

    def _gatherPluginHooks(self, path):
        '''Return plugin hooks from *path*.'''
        paths = []
        self.logger.debug(u'Searching {0!r} for plugin hooks.'.format(path))

        if os.path.isdir(path):
            for candidate in os.listdir(path):
                candidatePath = os.path.join(path, candidate)
                if os.path.isdir(candidatePath):
                    paths.append(
                        os.path.join(candidatePath, 'hook')
                    )

        self.logger.debug(
            u'Found {0!r} plugin hooks in {1!r}.'.format(paths, path)
        )

        return paths

    def _relayEventHubEvent(self, event):
        '''Relay all ftrack.connect events.'''
        self.eventHubSignal.emit(event)

    def _initialiseTray(self):
        '''Initialise and add application icon to system tray.'''
        self.trayMenu = self._createTrayMenu()

        self.tray = QtWidgets.QSystemTrayIcon(self)

        self.tray.setContextMenu(
            self.trayMenu
        )

        self.tray.setIcon(self.logoIcon)
        self.tray.show()

    def _createTrayMenu(self):
        '''Return a menu for system tray.'''
        menu = QtWidgets.QMenu(self)

        logoutAction = QtWidgets.QAction(
            'Log Out && Quit', self,
            triggered=self.logout
        )

        quitAction = QtWidgets.QAction(
            'Quit', self,
            triggered=QtWidgets.qApp.quit
        )

        focusAction = QtWidgets.QAction(
            'Open', self,
            triggered=self.focus
        )

        openPluginDirectoryAction = QtWidgets.QAction(
            'Open plugin directory', self,
            triggered=self.openDefaultPluginDirectory
        )

        aboutAction = QtWidgets.QAction(
            'About', self,
            triggered=self.showAbout
        )

        menu.addAction(aboutAction)
        menu.addAction(focusAction)
        menu.addSeparator()

        menu.addAction(openPluginDirectoryAction)
        menu.addSeparator()

        menu.addAction(logoutAction)
        menu.addSeparator()
        menu.addAction(quitAction)

        return menu

    def _discoverTabPlugins(self):
        '''Find and load tab plugins in search paths.'''
        #: TODO: Add discover functionality and search paths.

        from . import (publisher, actions)
        actions.register(self)
        publisher.register(self)

    def _routeEvent(self, event):
        '''Route websocket *event* to publisher plugin.

        Expect event['data'] to contain:

            * plugin - The name of the plugin to route to.
            * action - The action to execute on the plugin.

        Raise `ConnectError` if no plugin is found or if action is missing on
        plugin.

        '''
        plugin = event['data']['plugin']
        action = event['data']['action']

        try:
            pluginInstance = self.plugins[plugin]
        except KeyError:
            raise ftrack_connect.error.ConnectError(
                'Plugin "{0}" not found.'.format(
                    plugin
                )
            )

        try:
            method = getattr(pluginInstance, action)
        except AttributeError:
            raise ftrack_connect.error.ConnectError(
                'Method "{0}" not found on "{1}" plugin({2}).'.format(
                    action, plugin, pluginInstance
                )
            )

        method(event)

    def _onWidgetRequestApplicationFocus(self, widget):
        '''Switch tab to *widget* and bring application to front.'''
        self.tabPanel.setCurrentWidget(widget)
        self.focus()

    def _onWidgetRequestApplicationClose(self, widget):
        '''Hide application upon *widget* request.'''
        self.hide()

    def addPlugin(self, plugin, name=None, identifier=None):
        '''Add *plugin* in new tab with *name* and *identifier*.

        *plugin* should be an instance of :py:class:`ApplicationPlugin`.

        *name* will be used as the label for the tab. If *name* is None then
        plugin.getName() will be used.

        *identifier* will be used for routing events to plugins. If
        *identifier* is None then plugin.getIdentifier() will be used.

        '''
        if name is None:
            name = plugin.getName()

        if identifier is None:
            identifier = plugin.getIdentifier()

        if identifier in self.plugins:
            raise _NotUniqueError(
                'Cannot add plugin. An existing plugin has already been '
                'registered with identifier {0}.'.format(identifier)
            )

        self.plugins[identifier] = plugin
        self.tabPanel.addTab(plugin, name)

        # Connect standard plugin events.
        plugin.requestApplicationFocus.connect(
            self._onWidgetRequestApplicationFocus
        )
        plugin.requestApplicationClose.connect(
            self._onWidgetRequestApplicationClose
        )

    def removePlugin(self, identifier):
        '''Remove plugin registered with *identifier*.

        Raise :py:exc:`KeyError` if no plugin with *identifier* has been added.

        '''
        plugin = self.plugins.get(identifier)
        if plugin is None:
            raise KeyError(
                'No plugin registered with identifier "{0}".'.format(identifier)
            )

        index = self.tabPanel.indexOf(plugin)
        self.tabPanel.removeTab(index)

        plugin.deleteLater()
        del self.plugins[identifier]

    def focus(self):
        '''Focus and bring the window to top.'''
        self.activateWindow()
        self.show()
        self.raise_()

    def showAbout(self):
        '''Display window with about information.'''
        self.focus()

        aboutDialog = _about.AboutDialog(self)

        environmentData = os.environ.copy()
        environmentData.update({
            'PLATFORM': platform.platform(),
            'PYTHON_VERSION': platform.python_version()
        })

        versionData = [{
            'name': 'ftrack connect',
            'version': ftrack_connect.__version__,
            'core': True,
            'debug_information': environmentData
        }]

        # Import ftrack module and and try to get API version and
        # to load information from other plugins using hook.
        try:
            import ftrack
            apiVersion = ftrack.api.version_data.ftrackVersion
            environmentData['FTRACK_API_VERSION'] = apiVersion

            responses = ftrack.EVENT_HUB.publish(
                ftrack.Event(
                    'ftrack.connect.plugin.debug-information'
                ),
                synchronous=True
            )

            for response in responses:
                if isinstance(response, dict):
                    versionData.append(response)
                elif isinstance(response, list):
                    versionData = versionData + response

        except Exception:
            pass

        aboutDialog.setInformation(
            versionData=versionData,
            server=os.environ.get('FTRACK_SERVER', 'Not set'),
            user=getpass.getuser(),
        )

        aboutDialog.exec_()

    def openDefaultPluginDirectory(self):
        '''Open default plugin directory in platform default file browser.'''

        directory = self.defaultPluginDirectory

        if not os.path.exists(directory):
            # Create directory if not existing.
            try:
                os.makedirs(directory)
            except OSError:
                messageBox = QtWidgets.QMessageBox(parent=self)
                messageBox.setIcon(QtWidgets.QMessageBox.Warning)
                messageBox.setText(
                    u'Could not open or create default plugin '
                    u'directory: {0}.'.format(directory)
                )
                messageBox.exec_()
                return

        ftrack_connect.util.open_directory(directory)
Пример #29
0
class ContextSelector(QtWidgets.QFrame):
    '''Context browser with entity path field.'''

    entityChanged = QtCore.Signal(object)

    def __init__(self, currentEntity, parent=None):
        '''Initialise with the *currentEntity* and *parent* widget.'''
        super(ContextSelector, self).__init__(parent=parent)
        self.setObjectName('context-selector-widget')
        self._entity = currentEntity
        self.entityBrowser = EntityBrowser()
        self.entityBrowser.setMinimumWidth(600)
        self.entityPath = EntityPath()

        self.entityBrowseButton = QtWidgets.QPushButton('Change')
        self.entityBrowseButton.setFixedWidth(110)
        self.entityBrowseButton.setFixedHeight(35)

        layout = QtWidgets.QHBoxLayout()
        layout.setContentsMargins(10, 0, 10, 0)
        self.setMinimumHeight(50)
        self.setLayout(layout)

        layout.addWidget(self.entityPath)
        layout.addWidget(self.entityBrowseButton)

        self.entityBrowseButton.clicked.connect(
            self._onEntityBrowseButtonClicked
        )
        self.entityChanged.connect(self.entityPath.setEntity)
        self.entityBrowser.selectionChanged.connect(
            self._onEntityBrowserSelectionChanged
        )
        self.setEntity(self._entity)

    def reset(self, entity=None):
        '''Reset browser to the given *entity* or the default one.'''
        currentEntity = entity or self._entity
        self.entityPath.setEntity(currentEntity)
        self.setEntity(currentEntity)

    def setEntity(self, entity):
        '''Set the *entity* for the view.'''
        self._entity = entity
        self.entityChanged.emit(entity)

    def _onEntityBrowseButtonClicked(self):
        '''Handle entity browse button clicked.'''
        # Ensure browser points to parent of currently selected entity.
        if self._entity is not None:
            location = []
            try:
                parents = _get_entity_parents(self._entity)
            except AttributeError:
                pass
            else:
                for parent in parents:
                    location.append(parent['id'])

            self.entityBrowser.setLocation(location)

        # Launch browser.
        if self.entityBrowser.exec_():
            selected = self.entityBrowser.selected()
            session = selected[0].session

            if selected:
                self.setEntity(session.get('Context', selected[0]['id']))
            else:
                self.setEntity(None)

    def _onEntityBrowserSelectionChanged(self, selection):
        '''Handle selection of entity in browser.'''
        self.entityBrowser.acceptButton.setDisabled(True)

        # Only allow single select.
        if len(selection) == 1:
            # Do not allow selection of projects.
            if selection[0].entity_type == 'Project':
                return

            self.entityBrowser.acceptButton.setDisabled(False)
Пример #30
0
class UserList(ftrack_connect.ui.widget.item_list.ItemList):
    '''User list widget.'''

    #: Signal to handle click events.
    itemClicked = QtCore.Signal(object)

    def __init__(self, groups=None, parent=None):
        '''Initialise widget with *groups*.'''
        if groups is None:
            groups = []

        super(UserList,
              self).__init__(widgetFactory=self._createWidget,
                             widgetItem=lambda widget: widget.value(),
                             parent=parent)
        self.setObjectName('presence-list')
        self.list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
        self.list.setShowGrid(False)

        for group in groups:
            self.addUser(group.capitalize())

    def _createWidget(self, item):
        '''Return user widget for *item*.'''
        if item is None:
            item = {}

        if isinstance(item, basestring):
            return GroupHeader(item)

        widget = ftrack_connect.ui.widget.user.User(**item)
        widget.itemClicked.connect(self.itemClicked.emit)

        return widget

    def userExists(self, userId):
        '''Return true if *userId* exists.'''
        if self.getUser(userId):
            return True

        return False

    def getUser(self, userId):
        '''Return user if in list otherwise None.'''
        for row in range(self.count()):
            widget = self.list.widgetAt(row)
            value = self.widgetItem(widget)

            if isinstance(value, dict) and value['userId'] == userId:
                return widget

        return None

    def addUser(self, item, row=None):
        '''Add *item* at *row*.

        If *row* is not specified, then append item to end of list.

        '''
        if not isinstance(item, basestring):
            group = item.get('group')
            row = self.indexOfItem(group.capitalize())

            if row is None:
                raise ValueError('group {0} not recognized'.format(group))

            row += 1

        return super(UserList, self).addItem(item, row=row)

    def updatePosition(self, user):
        '''Update position of *user* based on user's group.'''

        if not user.online:
            user.group = 'offline'

        row = self.indexOfItem(user.group.capitalize())

        if row is None:
            raise ValueError('group {0} not recognized'.format(user.group))

        row += 1

        self.list.moveWidget(user, row)

    def users(self):
        '''Return list of users.'''
        users = []
        for item in self.items():
            if isinstance(item, dict):
                user = self.getUser(item.get('userId'))
                if user:
                    users.append(user)

        return users