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)
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(' ', '.')
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)
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))
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
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()
class ClickableLabel(QtWidgets.QLabel): '''Clickable label class.''' clicked = QtCore.Signal() def mousePressEvent(self, event): '''Override mouse press to emit signal.''' self.clicked.emit()
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()
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.'''
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
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'])
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) )
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()
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()
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()
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)
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
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))
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) )
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()
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)
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)
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()
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)
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)
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)
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