class Workbench(QMainWindow): """hg repository viewer/browser application""" finished = pyqtSignal(int) activeRepoChanged = pyqtSignal(QString) def __init__(self): QMainWindow.__init__(self) self.ui = ui.ui() self.setupUi() self.setWindowTitle(_('TortoiseHg Workbench')) self.reporegistry = rr = RepoRegistryView(self) rr.setObjectName('RepoRegistryView') rr.showMessage.connect(self.showMessage) rr.openRepo.connect(self.openRepo) rr.removeRepo.connect(self.removeRepo) rr.hide() self.addDockWidget(Qt.LeftDockWidgetArea, rr) self.activeRepoChanged.connect(rr.setActiveTabRepo) self.mqpatches = p = mq.MQPatchesWidget(self) p.setObjectName('MQPatchesWidget') p.showMessage.connect(self.showMessage) p.hide() self.addDockWidget(Qt.LeftDockWidgetArea, p) self.log = LogDockWidget(self) self.log.setObjectName('Log') self.log.progressReceived.connect(self.statusbar.progress) self.log.hide() self.addDockWidget(Qt.BottomDockWidgetArea, self.log) self._setupActions() self.restoreSettings() self.repoTabChanged() self.setAcceptDrops(True) if os.name == 'nt': # Allow CTRL+Q to close Workbench on Windows QShortcut(QKeySequence('CTRL+Q'), self, self.close) if sys.platform == 'darwin': self.dockMenu = QMenu(self) self.dockMenu.addAction(_('New Repository...'), self.newRepository) self.dockMenu.addAction(_('Clone Repository...'), self.cloneRepository) self.dockMenu.addAction(_('Open Repository...'), self.openRepository) qt_mac_set_dock_menu(self.dockMenu) # Create the actions that will be displayed on the context menu self.createActions() self.lastClosedRepoRootList = [] def setupUi(self): desktopgeom = qApp.desktop().availableGeometry() self.resize(desktopgeom.size() * 0.8) self.setWindowIcon(qtlib.geticon('hg-log')) self.repoTabsWidget = tw = QTabWidget() tw.setTabBar(ThgTabBar()) tw.setDocumentMode(True) tw.setTabsClosable(True) tw.setMovable(True) tw.tabBar().hide() tw.tabBar().setContextMenuPolicy(Qt.CustomContextMenu) tw.tabBar().customContextMenuRequested.connect(self.tabBarContextMenuRequest) tw.lastClickedTab = -1 # No tab clicked yet sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sp.setHorizontalStretch(1) sp.setVerticalStretch(1) sp.setHeightForWidth(tw.sizePolicy().hasHeightForWidth()) tw.setSizePolicy(sp) tw.tabCloseRequested.connect(self.repoTabCloseRequested) tw.currentChanged.connect(self.repoTabChanged) self.setCentralWidget(tw) self.statusbar = cmdui.ThgStatusBar(self) self.setStatusBar(self.statusbar) def _setupActions(self): """Setup actions, menus and toolbars""" self.menubar = QMenuBar(self) self.setMenuBar(self.menubar) self.menuFile = self.menubar.addMenu(_("&File")) self.menuView = self.menubar.addMenu(_("&View")) self.menuViewregistryopts = QMenu(_('Workbench Toolbars'), self) self.menuRepository = self.menubar.addMenu(_("&Repository")) self.menuHelp = self.menubar.addMenu(_("&Help")) self.edittbar = QToolBar(_("Edit Toolbar"), objectName='edittbar') self.addToolBar(self.edittbar) self.docktbar = QToolBar(_("Dock Toolbar"), objectName='docktbar') self.addToolBar(self.docktbar) self.synctbar = QToolBar(_('Sync Toolbar'), objectName='synctbar') self.addToolBar(self.synctbar) self.tasktbar = QToolBar(_('Task Toolbar'), objectName='taskbar') self.addToolBar(self.tasktbar) # availability map of actions; applied by updateMenu() self._actionavails = {'repoopen': []} def keysequence(o): """Create QKeySequence from string or QKeySequence""" if isinstance(o, (QKeySequence, QKeySequence.StandardKey)): return o try: return getattr(QKeySequence, str(o)) # standard key except AttributeError: return QKeySequence(o) def modifiedkeysequence(o, modifier): """Create QKeySequence of modifier key prepended""" origseq = QKeySequence(keysequence(o)) return QKeySequence('%s+%s' % (modifier, origseq.toString())) def newaction(text, slot=None, icon=None, shortcut=None, checkable=False, tooltip=None, data=None, enabled=None, menu=None, toolbar=None, parent=self): """Create new action and register it :slot: function called if action triggered or toggled. :checkable: checkable action. slot will be called on toggled. :data: optional data stored on QAction. :enabled: bool or group name to enable/disable action. :shortcut: QKeySequence, key sequence or name of standard key. :menu: name of menu to add this action. :toolbar: name of toolbar to add this action. """ action = QAction(text, parent, checkable=checkable) if slot: if checkable: action.toggled.connect(slot) else: action.triggered.connect(slot) if icon: if toolbar: action.setIcon(qtlib.geticon(icon)) else: action.setIcon(qtlib.getmenuicon(icon)) if shortcut: action.setShortcut(keysequence(shortcut)) if tooltip: action.setToolTip(tooltip) if data is not None: action.setData(data) if isinstance(enabled, bool): action.setEnabled(enabled) elif enabled: self._actionavails[enabled].append(action) if menu: getattr(self, 'menu%s' % menu.title()).addAction(action) if toolbar: getattr(self, '%stbar' % toolbar).addAction(action) return action def newseparator(menu=None, toolbar=None): """Insert a separator action; returns nothing""" if menu: getattr(self, 'menu%s' % menu.title()).addSeparator() if toolbar: getattr(self, '%stbar' % toolbar).addSeparator() newaction(_("&New Repository..."), self.newRepository, shortcut='New', menu='file', icon='hg-init') newaction(_("Clone Repository..."), self.cloneRepository, shortcut=modifiedkeysequence('New', modifier='Shift'), menu='file', icon='hg-clone') newseparator(menu='file') newaction(_("&Open Repository..."), self.openRepository, shortcut='Open', menu='file') closerepo = newaction(_("&Close Repository"), self.closeRepository, shortcut='Close', enabled='repoopen', menu='file') if os.name == 'nt': sc = closerepo.shortcuts() sc.append(keysequence('Ctrl+W')) closerepo.setShortcuts(sc) newseparator(menu='file') newaction(_('&Settings...'), self.editSettings, icon='settings_user', shortcut='Preferences', menu='file') newseparator(menu='file') newaction(_("E&xit"), self.close, shortcut='Quit', menu='file') a = self.reporegistry.toggleViewAction() a.setText(_('Show Repository Registry')) a.setShortcut('Ctrl+Shift+O') a.setIcon(qtlib.geticon('thg-reporegistry')) self.docktbar.addAction(a) self.menuView.addAction(a) a = self.mqpatches.toggleViewAction() a.setText(_('Show Patch Queue')) a.setIcon(qtlib.geticon('thg-mq')) self.docktbar.addAction(a) self.menuView.addAction(a) a = self.log.toggleViewAction() a.setText(_('Show Output &Log')) a.setShortcut('Ctrl+L') a.setIcon(qtlib.geticon('thg-console')) self.docktbar.addAction(a) self.menuView.addAction(a) newseparator(menu='view') self.menuViewregistryopts = self.menuView.addMenu(_('Repository Registry Options')) self.actionShowPaths = \ newaction(_("Show Paths"), self.reporegistry.showPaths, checkable=True, menu='viewregistryopts') self.actionShowSubrepos = \ newaction(_("Show Subrepos on Registry"), self.reporegistry.setShowSubrepos, checkable=True, menu='viewregistryopts') self.actionShowNetworkSubrepos = \ newaction(_("Show Subrepos for remote repositories"), self.reporegistry.setShowNetworkSubrepos, checkable=True, menu='viewregistryopts') self.actionShowShortPaths = \ newaction(_("Show Short Paths"), self.reporegistry.setShowShortPaths, checkable=True, menu='viewregistryopts') newseparator(menu='view') newaction(_("Choose Log Columns..."), self.setHistoryColumns, menu='view') self.actionSaveRepos = \ newaction(_("Save Open Repositories On Exit"), checkable=True, menu='view') newseparator(menu='view') self.actionGroupTaskView = QActionGroup(self) self.actionGroupTaskView.triggered.connect(self.onSwitchRepoTaskTab) def addtaskview(icon, label, name): a = newaction(label, icon=None, checkable=True, data=name, enabled='repoopen', menu='view') a.setIcon(qtlib.geticon(icon)) self.actionGroupTaskView.addAction(a) self.tasktbar.addAction(a) return a addtaskview('hg-log', _("Revision &Details"), 'log') addtaskview('hg-commit', _('&Commit'), 'commit') self.actionSelectTaskMQ = \ addtaskview('thg-qrefresh', _('MQ Patch'), 'mq') addtaskview('thg-sync', _('S&ynchronize'), 'sync') addtaskview('hg-annotate', _('&Manifest'), 'manifest') addtaskview('hg-grep', _('&Search'), 'grep') self.actionSelectTaskPbranch = \ addtaskview('branch', _('&Patch Branch'), 'pbranch') newseparator(menu='view') newaction(_("&Refresh"), self._repofwd('reload'), icon='view-refresh', shortcut='Refresh', enabled='repoopen', menu='view', toolbar='edit', tooltip=_('Refresh current repository')) newaction(_("Refresh &Task Tab"), self._repofwd('reloadTaskTab'), enabled='repoopen', shortcut=modifiedkeysequence('Refresh', modifier='Shift'), tooltip=_('Refresh only the current task tab'), menu='view') newaction(_("Load all revisions"), self.loadall, enabled='repoopen', menu='view', shortcut='Shift+Ctrl+A', tooltip=_('Load all revisions into graph')) newaction(_("Web Server..."), self.serve, enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("Shelve..."), self._repofwd('shelve'), icon='shelve', enabled='repoopen', menu='repository') newaction(_("Import..."), self._repofwd('thgimport'), icon='hg-import', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("Verify"), self._repofwd('verify'), enabled='repoopen', menu='repository') newaction(_("Recover"), self._repofwd('recover'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("Resolve..."), self._repofwd('resolve'), icon='hg-merge', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("Rollback/Undo..."), self._repofwd('rollback'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("Purge..."), self._repofwd('purge'), enabled='repoopen', icon='hg-purge', menu='repository') newseparator(menu='repository') newaction(_("Bisect..."), self._repofwd('bisect'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("Explore"), self.explore, shortcut='Shift+Ctrl+S', icon='system-file-manager', enabled='repoopen', menu='repository') newaction(_("Terminal"), self.terminal, shortcut='Shift+Ctrl+T', icon='utilities-terminal', enabled='repoopen', menu='repository') newaction(_("Help"), self.onHelp, menu='help', icon='help-browser') newaction(_("About Qt"), QApplication.aboutQt, menu='help') newaction(_("About TortoiseHg"), self.onAbout, menu='help', icon='thg-logo') newseparator(toolbar='edit') self.actionBack = \ newaction(_("Back"), self._repofwd('back'), icon='go-previous', enabled=False, toolbar='edit') self.actionForward = \ newaction(_("Forward"), self._repofwd('forward'), icon='go-next', enabled=False, toolbar='edit') newseparator(toolbar='edit', menu='View') self.filtertbaction = \ newaction(_('Filter Toolbar'), self._repotogglefwd('toggleFilterBar'), icon='view-filter', shortcut='Ctrl+S', enabled='repoopen', toolbar='edit', menu='View', checkable=True, tooltip=_('Filter graph with revision sets or branches')) menu = QMenu(_('Workbench Toolbars'), self) menu.addAction(self.edittbar.toggleViewAction()) menu.addAction(self.docktbar.toggleViewAction()) menu.addAction(self.synctbar.toggleViewAction()) menu.addAction(self.tasktbar.toggleViewAction()) self.menuView.addMenu(menu) newaction(_('Incoming'), self._repofwd('incoming'), icon='hg-incoming', tooltip=_('Check for incoming changes from selected URL'), enabled='repoopen', toolbar='sync') newaction(_('Pull'), self._repofwd('pull'), icon='hg-pull', tooltip=_('Pull incoming changes from selected URL'), enabled='repoopen', toolbar='sync') newaction(_('Outgoing'), self._repofwd('outgoing'), icon='hg-outgoing', tooltip=_('Detect outgoing changes to selected URL'), enabled='repoopen', toolbar='sync') newaction(_('Push'), self._repofwd('push'), icon='hg-push', tooltip=_('Push outgoing changes to selected URL'), enabled='repoopen', toolbar='sync') self.updateMenu() def _action_defs(self): a = [("closetab", _("Close tab"), '', _("Close tab"), self.closeLastClickedTab), ("closeothertabs", _("Close other tabs"), '', _("Close other tabs"), self.closeNotLastClickedTabs), ("reopenlastclosed", _("Undo close tab"), '', _("Reopen last closed tab"), self.reopenLastClosedTabs), ("reopenlastclosedgroup", _("Undo close other tabs"), '', _("Reopen last closed tab group"), self.reopenLastClosedTabs), ] return a def createActions(self): self._actions = {} for name, desc, icon, tip, cb in self._action_defs(): self._actions[name] = QAction(desc, self) QTimer.singleShot(0, self.configureActions) def configureActions(self): for name, desc, icon, tip, cb in self._action_defs(): act = self._actions[name] if icon: act.setIcon(qtlib.getmenuicon(icon)) if tip: act.setStatusTip(tip) if cb: act.triggered.connect(cb) self.addAction(act) @pyqtSlot(QPoint) def tabBarContextMenuRequest(self, point): # Activate the clicked tab clickedwidget = qApp.widgetAt(self.repoTabsWidget.mapToGlobal(point)) if not clickedwidget or \ not isinstance(clickedwidget, ThgTabBar): return self.repoTabsWidget.lastClickedTab = -1 clickedtabindex = clickedwidget.tabAt(point) if clickedtabindex > -1: self.repoTabsWidget.lastClickedTab = clickedtabindex else: self.repoTabsWidget.lastClickedTab = self.repoTabsWidget.currentIndex() actionlist = ['closetab', 'closeothertabs'] existingClosedRepoList = [] for reporoot in self.lastClosedRepoRootList: if os.path.isdir(reporoot): existingClosedRepoList.append(reporoot) self.lastClosedRepoRootList = existingClosedRepoList if len(self.lastClosedRepoRootList) > 1: actionlist += ['', 'reopenlastclosedgroup'] elif len(self.lastClosedRepoRootList) > 0: actionlist += ['', 'reopenlastclosed'] contextmenu = QMenu(self) for act in actionlist: if act: contextmenu.addAction(self._actions[act]) else: contextmenu.addSeparator() if actionlist: contextmenu.exec_(self.repoTabsWidget.mapToGlobal(point)) def closeLastClickedTab(self): if self.repoTabsWidget.lastClickedTab > -1: self.repoTabCloseRequested(self.repoTabsWidget.lastClickedTab) def _closeOtherTabs(self, tabIndex): if tabIndex > -1: tb = self.repoTabsWidget.tabBar() tb.setCurrentIndex(tabIndex) closedRepoRootList = [] for idx in range(tb.count()-1, -1, -1): if idx != tabIndex: self.repoTabCloseRequested(idx) # repoTabCloseRequested updates self.lastClosedRepoRootList closedRepoRootList += self.lastClosedRepoRootList self.lastClosedRepoRootList = closedRepoRootList def closeNotLastClickedTabs(self): self._closeOtherTabs(self.repoTabsWidget.lastClickedTab) def onSwitchRepoTaskTab(self, action): rw = self.repoTabsWidget.currentWidget() if rw: rw.switchToNamedTaskTab(str(action.data().toString())) @pyqtSlot(QString, bool) def openRepo(self, root, reuse): """ Open repo by openRepoSignal from reporegistry [unicode] """ root = hglib.fromunicode(root) self._openRepo(root, reuse) def removeRepo(self, root): """ Close tab if the repo is removed from reporegistry [unicode] """ root = hglib.fromunicode(root) for i in xrange(self.repoTabsWidget.count()): w = self.repoTabsWidget.widget(i) if hglib.tounicode(w.repo.root) == os.path.normpath(root): self.repoTabCloseRequested(i) return @pyqtSlot(QString) def openLinkedRepo(self, path): self.showRepo(path) rw = self.repoTabsWidget.currentWidget() if rw: rw.taskTabsWidget.setCurrentIndex(rw.commitTabIndex) @pyqtSlot(QString) def showRepo(self, root): """Activate the repo tab or open it if not available [unicode]""" root = hglib.fromunicode(root) for i in xrange(self.repoTabsWidget.count()): w = self.repoTabsWidget.widget(i) if hglib.tounicode(w.repo.root) == os.path.normpath(root): self.repoTabsWidget.setCurrentIndex(i) return self._openRepo(root, False) @pyqtSlot(unicode, QString) def setRevsetFilter(self, path, filter): for i in xrange(self.repoTabsWidget.count()): w = self.repoTabsWidget.widget(i) if hglib.tounicode(w.repo.root) == path: w.filterbar.revsetle.setText(filter) w.filterbar.returnPressed() return def find_root(self, url): p = hglib.fromunicode(url.toLocalFile()) return paths.find_root(p) def dragEnterEvent(self, event): d = event.mimeData() for u in d.urls(): root = self.find_root(u) if root: event.setDropAction(Qt.LinkAction) event.accept() break def dropEvent(self, event): accept = False d = event.mimeData() for u in d.urls(): root = self.find_root(u) if root: self.showRepo(hglib.tounicode(root)) accept = True if accept: event.setDropAction(Qt.LinkAction) event.accept() def updateMenu(self): """Enable actions when repoTabs are opened or closed or changed""" # Update actions affected by repo open/close someRepoOpen = self.repoTabsWidget.count() > 0 for action in self._actionavails['repoopen']: action.setEnabled(someRepoOpen) # Update actions affected by repo open/close/change self.updateTaskViewMenu() self.updateToolBarActions() tw = self.repoTabsWidget w = tw.currentWidget() if ((tw.count() == 0) or ((tw.count() == 1) and not self.ui.configbool('tortoisehg', 'forcerepotab', False))): tw.tabBar().hide() else: tw.tabBar().show() if tw.count() == 0: self.setWindowTitle(_('TortoiseHg Workbench')) elif w.repo.shortname != w.repo.displayname: self.setWindowTitle(_('%s - TortoiseHg Workbench - %s') % (w.repo.shortname, w.repo.displayname)) else: self.setWindowTitle(_('%s - TortoiseHg Workbench') % w.repo.shortname) def updateToolBarActions(self): w = self.repoTabsWidget.currentWidget() if w: self.filtertbaction.setChecked(w.filterBarVisible()) def updateTaskViewMenu(self): 'Update task tab menu for current repository' if self.repoTabsWidget.count() == 0: for a in self.actionGroupTaskView.actions(): a.setChecked(False) self.actionSelectTaskMQ.setVisible(False) self.actionSelectTaskPbranch.setVisible(False) else: repoWidget = self.repoTabsWidget.currentWidget() exts = repoWidget.repo.extensions() self.actionSelectTaskMQ.setVisible('mq' in exts) self.actionSelectTaskPbranch.setVisible('pbranch' in exts) taskIndex = repoWidget.taskTabsWidget.currentIndex() for name, idx in repoWidget.namedTabs.iteritems(): if idx == taskIndex: break for action in self.actionGroupTaskView.actions(): if str(action.data().toString()) == name: action.setChecked(True) @pyqtSlot() def updateHistoryActions(self): 'Update back / forward actions' rw = self.repoTabsWidget.currentWidget() if not rw: return self.actionBack.setEnabled(rw.canGoBack()) self.actionForward.setEnabled(rw.canGoForward()) def repoTabCloseSelf(self, widget): self.repoTabsWidget.setCurrentWidget(widget) index = self.repoTabsWidget.currentIndex() if widget.closeRepoWidget(): w = self.repoTabsWidget.widget(index) try: reporoot = w.repo.root except: reporoot = '' self.repoTabsWidget.removeTab(index) widget.deleteLater() self.updateMenu() self.lastClosedRepoRootList = [reporoot] def repoTabCloseRequested(self, index): tw = self.repoTabsWidget if 0 <= index < tw.count(): w = tw.widget(index) try: reporoot = w.repo.root except: reporoot = '' if w and w.closeRepoWidget(): tw.removeTab(index) w.deleteLater() self.updateMenu() self.lastClosedRepoRootList = [reporoot] def reopenLastClosedTabs(self): for reporoot in self.lastClosedRepoRootList: if os.path.isdir(reporoot): self.showRepo(reporoot) self.lastClosedRepoRootList = [] def repoTabChanged(self, index=0): w = self.repoTabsWidget.currentWidget() if w: self.updateHistoryActions() self.updateMenu() if w.repo: root = w.repo.root self.activeRepoChanged.emit(hglib.tounicode(root)) else: self.activeRepoChanged.emit("") repo = w and w.repo or None self.log.setRepository(repo) self.mqpatches.setrepo(repo) def addRepoTab(self, repo): '''opens the given repo in a new tab''' rw = RepoWidget(repo, self) rw.showMessageSignal.connect(self.showMessage) rw.closeSelfSignal.connect(self.repoTabCloseSelf) rw.progress.connect(lambda tp, p, i, u, tl: self.statusbar.progress(tp, p, i, u, tl, repo.root)) rw.output.connect(self.log.output) rw.makeLogVisible.connect(self.log.setShown) rw.beginSuppressPrompt.connect(self.log.beginSuppressPrompt) rw.endSuppressPrompt.connect(self.log.endSuppressPrompt) rw.revisionSelected.connect(self.updateHistoryActions) rw.repoLinkClicked.connect(self.openLinkedRepo) rw.taskTabsWidget.currentChanged.connect(self.updateTaskViewMenu) rw.toolbarVisibilityChanged.connect(self.updateToolBarActions) rw.shortNameChanged.connect(self.reporegistry.shortNameChanged) rw.baseNodeChanged.connect(self.reporegistry.baseNodeChanged) rw.repoChanged.connect(self.reporegistry.repoChanged) tw = self.repoTabsWidget index = self.repoTabsWidget.addTab(rw, rw.title()) tw.setCurrentIndex(index) rw.titleChanged.connect( lambda title: tw.setTabText(tw.indexOf(rw), title)) rw.showIcon.connect( lambda icon: tw.setTabIcon(tw.indexOf(rw), icon)) self.reporegistry.addRepo(repo.root) self.updateMenu() def showMessage(self, msg): self.statusbar.showMessage(msg) def setHistoryColumns(self, *args): """Display the column selection dialog""" w = self.repoTabsWidget.currentWidget() dlg = ColumnSelectDialog('workbench', _('Workbench'), w and w.repoview.model() or None) if dlg.exec_() == QDialog.Accepted: if w: w.repoview.model().updateColumns() w.repoview.resizeColumns() def _repotogglefwd(self, name): """Return function to forward action to the current repo tab""" def forwarder(checked): w = self.repoTabsWidget.currentWidget() if w: getattr(w, name)(checked) return forwarder def _repofwd(self, name): """Return function to forward action to the current repo tab""" def forwarder(): w = self.repoTabsWidget.currentWidget() if w: getattr(w, name)() return forwarder def serve(self): w = self.repoTabsWidget.currentWidget() if w: from tortoisehg.hgqt import run run.serve(w.repo.ui, root=w.repo.root) def loadall(self): w = self.repoTabsWidget.currentWidget() if w: w.repoview.model().loadall() def newRepository(self): """ Run init dialog """ from tortoisehg.hgqt.hginit import InitDialog repoWidget = self.repoTabsWidget.currentWidget() if repoWidget: path = os.path.dirname(repoWidget.repo.root) else: path = os.getcwd() dlg = InitDialog([path], parent=self) dlg.finished.connect(dlg.deleteLater) if dlg.exec_(): path = dlg.getPath() self._openRepo(path, False) def cloneRepository(self): """ Run clone dialog """ from tortoisehg.hgqt.clone import CloneDialog repoWidget = self.repoTabsWidget.currentWidget() if repoWidget: root = repoWidget.repo.root args = [root, root + '-clone'] else: args = [] dlg = CloneDialog(args, parent=self) dlg.finished.connect(dlg.deleteLater) dlg.clonedRepository.connect(self.showRepo) dlg.exec_() def openRepository(self): """ Open repo from File menu """ caption = _('Select repository directory to open') repoWidget = self.repoTabsWidget.currentWidget() if repoWidget: cwd = os.path.dirname(repoWidget.repo.root) else: cwd = os.getcwd() cwd = hglib.tounicode(cwd) FD = QFileDialog path = FD.getExistingDirectory(self, caption, cwd, FD.ShowDirsOnly | FD.ReadOnly) self._openRepo(hglib.fromunicode(path), False) def _openRepo(self, root, reuse): if root and not root.startswith('ssh://'): if reuse: for rw in self._findrepowidget(root): self.repoTabsWidget.setCurrentWidget(rw) return try: repo = thgrepo.repository(path=root) self.addRepoTab(repo) except RepoError: upath = hglib.tounicode(root) qtlib.WarningMsgBox(_('Failed to open repository'), _('%s is not a valid repository') % upath) def _findrepowidget(self, root): """Iterates RepoWidget for the specified root""" tw = self.repoTabsWidget for idx in range(tw.count()): rw = tw.widget(idx) if rw.repo.root == root: yield rw def onAbout(self, *args): """ Display about dialog """ from tortoisehg.hgqt.about import AboutDialog ad = AboutDialog(self) ad.finished.connect(ad.deleteLater) ad.exec_() def onHelp(self, *args): """ Display online help """ qtlib.openhelpcontents('workbench.html') def storeSettings(self): s = QSettings() wb = "Workbench/" s.setValue(wb + 'geometry', self.saveGeometry()) s.setValue(wb + 'windowState', self.saveState()) s.setValue(wb + 'showPaths', self.actionShowPaths.isChecked()) s.setValue(wb + 'showSubrepos', self.actionShowSubrepos.isChecked()) s.setValue(wb + 'showNetworkSubrepos', self.actionShowNetworkSubrepos.isChecked()) s.setValue(wb + 'showShortPaths', self.actionShowShortPaths.isChecked()) s.setValue(wb + 'saveRepos', self.actionSaveRepos.isChecked()) repostosave = [] if self.actionSaveRepos.isChecked(): tw = self.repoTabsWidget for idx in range(tw.count()): rw = tw.widget(idx) repostosave.append(hglib.tounicode(rw.repo.root)) s.setValue(wb + 'openrepos', (',').join(repostosave)) def restoreSettings(self): s = QSettings() wb = "Workbench/" self.restoreGeometry(s.value(wb + 'geometry').toByteArray()) self.restoreState(s.value(wb + 'windowState').toByteArray()) # Load the repo registry settings. Note that we must allow the # repo registry to assemble itself before toggling its settings # Also the view path setttings should be enabled last, once we have # loaded the repo subrepositories (if needed) # Normally, checking the "show subrepos" and the "show network subrepos" # settings will trigger a reload of the repo registry. # To avoid reloading it twice (every time we set one of its view # settings), we tell the setters to avoid reloading the repo tree # model, and then we manually reload the model ssr = s.value(wb + 'showSubrepos', defaultValue=QVariant(True)).toBool() snsr = s.value(wb + 'showNetworkSubrepos', defaultValue=QVariant(True)).toBool() ssp = s.value(wb + 'showShortPaths', defaultValue=QVariant(True)).toBool() self.reporegistry.setShowSubrepos(ssr, False) self.reporegistry.setShowNetworkSubrepos(snsr, False) self.reporegistry.setShowShortPaths(ssp) # Note that calling setChecked will NOT reload the model if the new # setting is the same as the one in the repo registry QTimer.singleShot(0, lambda: self.actionShowSubrepos.setChecked(ssr)) QTimer.singleShot(0, lambda: self.actionShowNetworkSubrepos.setChecked(ssr)) QTimer.singleShot(0, lambda: self.actionShowShortPaths.setChecked(ssp)) # Manually reload the model now, to apply the settings self.reporegistry.reloadModel() save = s.value(wb + 'saveRepos').toBool() self.actionSaveRepos.setChecked(save) for path in hglib.fromunicode(s.value(wb + 'openrepos').toString()).split(','): self._openRepo(path, False) # Allow repo registry to assemble itself before toggling path state sp = s.value(wb + 'showPaths').toBool() QTimer.singleShot(0, lambda: self.actionShowPaths.setChecked(sp)) def goto(self, root, rev): for rw in self._findrepowidget(root): rw.goto(rev) def closeEvent(self, event): if not self.closeRepoTabs(): event.ignore() else: self.storeSettings() self.reporegistry.close() # mimic QDialog exit self.finished.emit(0) def closeRepoTabs(self): '''returns False if close should be aborted''' tw = self.repoTabsWidget for idx in range(tw.count()): rw = tw.widget(idx) if not rw.closeRepoWidget(): tw.setCurrentWidget(rw) return False return True def closeRepository(self): """close the current repo tab""" self.repoTabCloseRequested(self.repoTabsWidget.currentIndex()) def explore(self): w = self.repoTabsWidget.currentWidget() if w: qtlib.openlocalurl(w.repo.root) def terminal(self): w = self.repoTabsWidget.currentWidget() if w: qtlib.openshell(w.repo.root, w.repo.displayname) def editSettings(self): tw = self.repoTabsWidget w = tw.currentWidget() twrepo = (w and w.repo.root or '') sd = SettingsDialog(configrepo=False, focus='tortoisehg.authorcolor', parent=self, root=twrepo) sd.exec_()
class Workbench(QMainWindow): """hg repository viewer/browser application""" def __init__(self, ui, repomanager): QMainWindow.__init__(self) self.ui = ui self._repomanager = repomanager self._repomanager.configChanged.connect(self._setupUrlComboIfCurrent) self.setupUi() repomanager.busyChanged.connect(self._onBusyChanged) repomanager.progressReceived.connect(self.statusbar.setRepoProgress) self.reporegistry = rr = RepoRegistryView(repomanager, self) rr.setObjectName('RepoRegistryView') rr.showMessage.connect(self.statusbar.showMessage) rr.openRepo.connect(self.openRepo) rr.removeRepo.connect(self.repoTabsWidget.closeRepo) rr.cloneRepoRequested.connect(self.cloneRepository) rr.progressReceived.connect(self.statusbar.progress) self._repomanager.repositoryChanged.connect(rr.scanRepo) rr.hide() self.addDockWidget(Qt.LeftDockWidgetArea, rr) self.mqpatches = p = mq.MQPatchesWidget(self) p.setObjectName('MQPatchesWidget') p.patchSelected.connect(self.gotorev) p.hide() self.addDockWidget(Qt.LeftDockWidgetArea, p) cmdagent = cmdcore.CmdAgent(ui, self) self._console = LogDockWidget(repomanager, cmdagent, self) self._console.setObjectName('Log') self._console.hide() self._console.visibilityChanged.connect(self._updateShowConsoleAction) self.addDockWidget(Qt.BottomDockWidgetArea, self._console) self._setupActions() self.restoreSettings() self.repoTabChanged() self.setAcceptDrops(True) self.setIconSize(qtlib.toolBarIconSize()) if os.name == 'nt': # Allow CTRL+Q to close Workbench on Windows QShortcut(QKeySequence('CTRL+Q'), self, self.close) if sys.platform == 'darwin': self.dockMenu = QMenu(self) self.dockMenu.addAction(_('New &Workbench'), self.newWorkbench) self.dockMenu.addAction(_('&New Repository...'), self.newRepository) self.dockMenu.addAction(_('Clon&e Repository...'), self.cloneRepository) self.dockMenu.addAction(_('&Open Repository...'), self.openRepository) qt_mac_set_dock_menu(self.dockMenu) # On Mac OS X, we do not want icons on menus qt_mac_set_menubar_icons(False) self._dialogs = qtlib.DialogKeeper( lambda self, dlgmeth: dlgmeth(self), parent=self) def setupUi(self): desktopgeom = qApp.desktop().availableGeometry() self.resize(desktopgeom.size() * 0.8) self.repoTabsWidget = tw = repotab.RepoTabWidget( self.ui, self._repomanager, self) sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sp.setHorizontalStretch(1) sp.setVerticalStretch(1) sp.setHeightForWidth(tw.sizePolicy().hasHeightForWidth()) tw.setSizePolicy(sp) tw.currentTabChanged.connect(self.repoTabChanged) tw.currentRepoChanged.connect(self._onCurrentRepoChanged) tw.currentTaskTabChanged.connect(self._updateTaskViewMenu) tw.currentTitleChanged.connect(self._updateWindowTitle) tw.historyChanged.connect(self._updateHistoryActions) tw.makeLogVisible.connect(self._setConsoleVisible) tw.toolbarVisibilityChanged.connect(self._updateToolBarActions) self.setCentralWidget(tw) self.statusbar = cmdui.ThgStatusBar(self) self.setStatusBar(self.statusbar) tw.progressReceived.connect(self.statusbar.setRepoProgress) tw.showMessageSignal.connect(self.statusbar.showMessage) def _setupActions(self): """Setup actions, menus and toolbars""" self.menubar = QMenuBar(self) self.setMenuBar(self.menubar) self.menuFile = self.menubar.addMenu(_("&File")) self.menuView = self.menubar.addMenu(_("&View")) self.menuRepository = self.menubar.addMenu(_("&Repository")) self.menuHelp = self.menubar.addMenu(_("&Help")) self.edittbar = QToolBar(_("&Edit Toolbar"), objectName='edittbar') self.addToolBar(self.edittbar) self.docktbar = QToolBar(_("&Dock Toolbar"), objectName='docktbar') self.addToolBar(self.docktbar) self.tasktbar = QToolBar(_('&Task Toolbar'), objectName='taskbar') self.addToolBar(self.tasktbar) self.customtbar = QToolBar(_('&Custom Toolbar'), objectName='custombar') self.addToolBar(self.customtbar) self.synctbar = QToolBar(_('S&ync Toolbar'), objectName='synctbar') self.addToolBar(self.synctbar) # availability map of actions; applied by _updateMenu() self._actionavails = {'repoopen': []} self._actionvisibles = {'repoopen': []} modifiedkeysequence = qtlib.modifiedkeysequence newaction = self._addNewAction newseparator = self._addNewSeparator newaction(_("New &Workbench"), self.newWorkbench, shortcut='Shift+Ctrl+W', menu='file', icon='hg-log') newseparator(menu='file') newaction(_("&New Repository..."), self.newRepository, shortcut='New', menu='file', icon='hg-init') newaction(_("Clon&e Repository..."), self.cloneRepository, shortcut=modifiedkeysequence('New', modifier='Shift'), menu='file', icon='hg-clone') newseparator(menu='file') newaction(_("&Open Repository..."), self.openRepository, shortcut='Open', menu='file') newaction(_("&Close Repository"), self.closeCurrentRepoTab, shortcut='Close', enabled='repoopen', menu='file') newseparator(menu='file') newaction(_('&Settings'), self.editSettings, icon='thg-userconfig', shortcut='Preferences', menu='file') newseparator(menu='file') newaction(_("E&xit"), self.close, shortcut='Quit', menu='file') a = self.reporegistry.toggleViewAction() a.setText(_('Sh&ow Repository Registry')) a.setShortcut('Ctrl+Shift+O') a.setIcon(qtlib.geticon('thg-reporegistry')) self.docktbar.addAction(a) self.menuView.addAction(a) a = self.mqpatches.toggleViewAction() a.setText(_('Show &Patch Queue')) a.setIcon(qtlib.geticon('thg-mq')) self.docktbar.addAction(a) self.menuView.addAction(a) self._actionShowConsole = a = QAction(_('Show Conso&le'), self) a.setCheckable(True) a.setShortcut('Ctrl+L') a.setIcon(qtlib.geticon('thg-console')) a.triggered.connect(self._setConsoleVisible) self.docktbar.addAction(a) self.menuView.addAction(a) self._actionDockedConsole = a = QAction(self) a.setText(_('Place Console in Doc&k Area')) a.setCheckable(True) a.setChecked(True) a.triggered.connect(self._updateDockedConsoleMode) newseparator(menu='view') menu = self.menuView.addMenu(_('R&epository Registry Options')) menu.addActions(self.reporegistry.settingActions()) newseparator(menu='view') newaction(_("C&hoose Log Columns..."), self._setHistoryColumns, enabled='repoopen', menu='view') self.actionSaveRepos = \ newaction(_("Save Open Repositories on E&xit"), checkable=True, menu='view') self.actionSaveLastSyncPaths = \ newaction(_("Sa&ve Current Sync Paths on Exit"), checkable=True, menu='view') newseparator(menu='view') self.actionGroupTaskView = QActionGroup(self) self.actionGroupTaskView.triggered.connect(self._onSwitchRepoTaskTab) def addtaskview(icon, label, name): a = newaction(label, icon=None, checkable=True, data=name, enabled='repoopen', menu='view') a.setIcon(qtlib.geticon(icon)) self.actionGroupTaskView.addAction(a) self.tasktbar.addAction(a) return a # note that 'grep' and 'search' are equivalent taskdefs = { 'commit': ('hg-commit', _('&Commit')), 'pbranch': ('hg-branch', _('&Patch Branch')), 'log': ('hg-log', _("Revision &Details")), 'grep': ('hg-grep', _('&Search')), 'sync': ('thg-sync', _('S&ynchronize')), # 'console' is toggled by "Show Console" action } tasklist = self.ui.configlist( 'tortoisehg', 'workbench.task-toolbar', []) if tasklist == []: tasklist = ['log', 'commit', 'grep', 'pbranch', '|', 'sync'] self.actionSelectTaskPbranch = None for taskname in tasklist: taskname = taskname.strip() taskinfo = taskdefs.get(taskname, None) if taskinfo is None: newseparator(toolbar='task') continue tbar = addtaskview(taskinfo[0], taskinfo[1], taskname) if taskname == 'pbranch': self.actionSelectTaskPbranch = tbar newseparator(menu='view') a = newaction(_("&Refresh"), self.refresh, icon='view-refresh', enabled='repoopen', menu='view', toolbar='edit', tooltip=_('Refresh current repository')) a.setShortcuts(QKeySequence.keyBindings(QKeySequence.Refresh) + [QKeySequence('Ctrl+F5')]) # Ctrl+ to ignore status newaction(_("Refresh &Task Tab"), self._repofwd('reloadTaskTab'), enabled='repoopen', shortcut=modifiedkeysequence('Refresh', modifier='Shift'), tooltip=_('Refresh only the current task tab'), menu='view') newaction(_("Load &All Revisions"), self.loadall, enabled='repoopen', menu='view', shortcut='Shift+Ctrl+A', tooltip=_('Load all revisions into graph')) self.actionAbort = \ newaction(_('Cancel'), self._abortCommands, icon='process-stop', toolbar='edit', tooltip=_('Stop current operation')) self.actionAbort.setEnabled(False) newseparator(toolbar='edit') newaction(_("Go to current revision"), self._repofwd('gotoParent'), icon='go-home', tooltip=_('Go to current revision'), enabled='repoopen', toolbar='edit', shortcut='Ctrl+.') newaction(_("&Goto Revision..."), self._gotorev, icon='go-to-rev', shortcut='Ctrl+/', enabled='repoopen', tooltip=_('Go to a specific revision'), menu='view', toolbar='edit') self.actionBack = \ newaction(_("Back"), self._repofwd('back'), icon='go-previous', shortcut=QKeySequence.Back, enabled=False, toolbar='edit') self.actionForward = \ newaction(_("Forward"), self._repofwd('forward'), icon='go-next', shortcut=QKeySequence.Forward, enabled=False, toolbar='edit') newseparator(toolbar='edit', menu='View') self.filtertbaction = \ newaction(_('&Filter Toolbar'), self._repotogglefwd('toggleFilterBar'), icon='view-filter', shortcut='Ctrl+S', enabled='repoopen', toolbar='edit', menu='View', checkable=True, tooltip=_('Filter graph with revision sets or branches')) menu = QMenu(_('&Workbench Toolbars'), self) menu.addAction(self.edittbar.toggleViewAction()) menu.addAction(self.docktbar.toggleViewAction()) menu.addAction(self.tasktbar.toggleViewAction()) menu.addAction(self.synctbar.toggleViewAction()) menu.addAction(self.customtbar.toggleViewAction()) self.menuView.addMenu(menu) newseparator(toolbar='edit') menuSync = self.menuRepository.addMenu(_('S&ynchronize')) a = newaction(_("&Lock File..."), self._repofwd('lockTool'), icon='thg-password', enabled='repoopen', menu='repository', toolbar='edit', tooltip=_('Lock or unlock files')) self.lockToolAction = a newseparator(menu='repository') newaction(_("&Update..."), self._repofwd('updateToRevision'), icon='hg-update', enabled='repoopen', menu='repository', toolbar='edit', tooltip=_('Update working directory or switch revisions')) newaction(_("&Shelve..."), self._repofwd('shelve'), icon='hg-shelve', enabled='repoopen', menu='repository') newaction(_("&Import Patches..."), self._repofwd('thgimport'), icon='hg-import', enabled='repoopen', menu='repository') newaction(_("U&nbundle..."), self._repofwd('unbundle'), icon='hg-unbundle', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_('&Merge...'), self._repofwd('mergeWithOtherHead'), icon='hg-merge', enabled='repoopen', menu='repository', toolbar='edit', tooltip=_('Merge with the other head of the current branch')) newaction(_("&Resolve..."), self._repofwd('resolve'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("R&ollback/Undo..."), self._repofwd('rollback'), shortcut='Ctrl+u', enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("&Purge..."), self._repofwd('purge'), enabled='repoopen', icon='hg-purge', menu='repository') newseparator(menu='repository') newaction(_("&Bisect..."), self._repofwd('bisect'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("&Verify"), self._repofwd('verify'), enabled='repoopen', menu='repository') newaction(_("Re&cover"), self._repofwd('recover'), enabled='repoopen', menu='repository') newseparator(menu='repository') newaction(_("E&xplore"), self.explore, shortcut='Shift+Ctrl+X', icon='system-file-manager', enabled='repoopen', menu='repository') newaction(_("&Terminal"), self.terminal, shortcut='Shift+Ctrl+T', icon='utilities-terminal', enabled='repoopen', menu='repository') newaction(_("&Web Server"), self.serve, menu='repository', icon='hg-serve') newaction(_("&Help"), self.onHelp, menu='help', icon='help-browser') newaction(_("E&xplorer Help"), self.onHelpExplorer, menu='help') visiblereadme = 'repoopen' if self.ui.config('tortoisehg', 'readme', None): visiblereadme = True newaction(_("&Readme"), self.onReadme, menu='help', icon='help-readme', visible=visiblereadme, shortcut='Ctrl+F1') newseparator(menu='help') newaction(_("About &Qt"), QApplication.aboutQt, menu='help') newaction(_("&About TortoiseHg"), self.onAbout, menu='help', icon='thg') newaction(_('&Incoming'), data='incoming', icon='hg-incoming', enabled='repoopen', toolbar='sync', shortcut='Ctrl+Shift+,') newaction(_('&Pull'), data='pull', icon='hg-pull', enabled='repoopen', toolbar='sync') newaction(_('&Outgoing'), data='outgoing', icon='hg-outgoing', enabled='repoopen', toolbar='sync', shortcut='Ctrl+Shift+.') newaction(_('P&ush'), data='push', icon='hg-push', enabled='repoopen', toolbar='sync') menuSync.addActions(self.synctbar.actions()) menuSync.addSeparator() action = QAction(_('&Sync Bookmarks...'), self) action.setIcon(qtlib.geticon('thg-sync-bookmarks')) self._actionavails['repoopen'].append(action) action.triggered.connect(self._runSyncBookmarks) menuSync.addAction(action) self._lastRepoSyncPath = {} self.urlCombo = QComboBox(self) self.urlCombo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.urlCombo.currentIndexChanged.connect(self._updateSyncUrl) self.urlComboAction = self.synctbar.addWidget(self.urlCombo) # hide it because workbench could be started without open repo self.urlComboAction.setVisible(False) self.synctbar.actionTriggered.connect(self._runSyncAction) def _setupUrlCombo(self, repo): """repository has been switched, fill urlCombo with URLs""" pathdict = dict((hglib.tounicode(alias), hglib.tounicode(path)) for alias, path in repo.ui.configitems('paths')) aliases = pathdict.keys() combo_setting = repo.ui.config('tortoisehg', 'workbench.target-combo', 'auto') self.urlComboAction.setVisible(len(aliases) > 1 or combo_setting == 'always') # 1. Sort the list if aliases aliases.sort() # 2. Place the default alias at the top of the list if 'default' in aliases: aliases.remove('default') aliases.insert(0, 'default') # 3. Make a list of paths that have a 'push path' # note that the default path will be first (if it has a push path), # followed by the other paths that have a push path, alphabetically haspushaliases = [alias for alias in aliases if alias + '-push' in aliases] # 4. Place the "-push" paths next to their "pull paths" regularaliases = [] for a in aliases[:]: if a.endswith('-push'): if a[:-len('-push')] in haspushaliases: continue regularaliases.append(a) if a in haspushaliases: regularaliases.append(a + '-push') # 5. Create the list of 'combined aliases' combinedaliases = [(a, a + '-push') for a in haspushaliases] # 6. Put the combined aliases first, followed by the regular aliases aliases = combinedaliases + regularaliases # 7. Ensure the first path is a default path (either a # combined "default | default-push" path or a regular default path) if not 'default-push' in aliases and 'default' in aliases: aliases.remove('default') aliases.insert(0, 'default') self.urlCombo.blockSignals(True) self.urlCombo.clear() for n, a in enumerate(aliases): # text, (pull-alias, push-alias) if isinstance(a, tuple): itemtext = u'\u2193 %s | %s \u2191' % a itemdata = tuple(pathdict[alias] for alias in a) tooltip = _('pull: %s\npush: %s') % itemdata else: itemtext = a itemdata = (pathdict[a], pathdict[a]) tooltip = pathdict[a] self.urlCombo.addItem(itemtext, itemdata) self.urlCombo.setItemData(n, tooltip, Qt.ToolTipRole) # Try to select the previously selected path, if any prevpath = self._lastRepoSyncPath.get(hglib.tounicode(repo.root)) if prevpath: idx = self.urlCombo.findText(prevpath) if idx >= 0: self.urlCombo.setCurrentIndex(idx) self.urlCombo.blockSignals(False) self._updateSyncUrlToolTip(self.urlCombo.currentIndex()) @pyqtSlot(str) def _setupUrlComboIfCurrent(self, root): w = self._currentRepoWidget() if w.repoRootPath() == root: self._setupUrlCombo(w.repo) def _syncUrlFor(self, op): """Current URL for the given sync operation""" urlindex = self.urlCombo.currentIndex() if urlindex < 0: return opindex = {'incoming': 0, 'pull': 0, 'outgoing': 1, 'push': 1}[op] return self.urlCombo.itemData(urlindex).toPyObject()[opindex] @pyqtSlot(int) def _updateSyncUrl(self, index): self._updateSyncUrlToolTip(index) # save the new url for later recovery reporoot = self.currentRepoRootPath() if not reporoot: return path = self.urlCombo.currentText() self._lastRepoSyncPath[reporoot] = path def _updateSyncUrlToolTip(self, index): self._updateUrlComboToolTip(index) self._updateSyncActionToolTip(index) def _updateUrlComboToolTip(self, index): if not self.urlCombo.count(): tooltip = _('There are no configured sync paths.\n' 'Open the Synchronize tab to configure them.') else: tooltip = self.urlCombo.itemData(index, Qt.ToolTipRole).toString() self.urlCombo.setToolTip(tooltip) def _updateSyncActionToolTip(self, index): if index < 0: tooltips = { 'incoming': _('Check for incoming changes'), 'pull': _('Pull incoming changes'), 'outgoing': _('Detect outgoing changes'), 'push': _('Push outgoing changes'), } else: pullurl, pushurl = self.urlCombo.itemData(index).toPyObject() tooltips = { 'incoming': _('Check for incoming changes from\n%s') % pullurl, 'pull': _('Pull incoming changes from\n%s') % pullurl, 'outgoing': _('Detect outgoing changes to\n%s') % pushurl, 'push': _('Push outgoing changes to\n%s') % pushurl, } for a in self.synctbar.actions(): op = str(a.data().toString()) if op in tooltips: a.setToolTip(tooltips[op]) def _setupCustomTools(self, ui): tools, toollist = hglib.tortoisehgtools(ui, selectedlocation='workbench.custom-toolbar') # Clear the existing "custom" toolbar self.customtbar.clear() # and repopulate it again with the tool configuration # for the current repository if not tools: return for name in toollist: if name == '|': self._addNewSeparator(toolbar='custom') continue info = tools.get(name, None) if info is None: continue command = info.get('command', None) if not command: continue showoutput = info.get('showoutput', False) workingdir = info.get('workingdir', '') label = info.get('label', name) tooltip = info.get('tooltip', _("Execute custom tool '%s'") % label) icon = info.get('icon', 'tools-spanner-hammer') self._addNewAction(label, self._repofwd('runCustomCommand', [command, showoutput, workingdir]), icon=icon, tooltip=tooltip, enabled=True, toolbar='custom') def _addNewAction(self, text, slot=None, icon=None, shortcut=None, checkable=False, tooltip=None, data=None, enabled=None, visible=None, menu=None, toolbar=None): """Create new action and register it :slot: function called if action triggered or toggled. :checkable: checkable action. slot will be called on toggled. :data: optional data stored on QAction. :enabled: bool or group name to enable/disable action. :visible: bool or group name to show/hide action. :shortcut: QKeySequence, key sequence or name of standard key. :menu: name of menu to add this action. :toolbar: name of toolbar to add this action. """ action = QAction(text, self, checkable=checkable) if slot: if checkable: action.toggled.connect(slot) else: action.triggered.connect(slot) if icon: action.setIcon(qtlib.geticon(icon)) if shortcut: keyseq = qtlib.keysequence(shortcut) if isinstance(keyseq, QKeySequence.StandardKey): action.setShortcuts(keyseq) else: action.setShortcut(keyseq) if tooltip: if action.shortcut(): tooltip += ' (%s)' % action.shortcut().toString() action.setToolTip(tooltip) if data is not None: action.setData(data) if isinstance(enabled, bool): action.setEnabled(enabled) elif enabled: self._actionavails[enabled].append(action) if isinstance(visible, bool): action.setVisible(visible) elif visible: self._actionvisibles[visible].append(action) if menu: getattr(self, 'menu%s' % menu.title()).addAction(action) if toolbar: getattr(self, '%stbar' % toolbar).addAction(action) return action def _addNewSeparator(self, menu=None, toolbar=None): """Insert a separator action; returns nothing""" if menu: getattr(self, 'menu%s' % menu.title()).addSeparator() if toolbar: getattr(self, '%stbar' % toolbar).addSeparator() def createPopupMenu(self): """Create new popup menu for toolbars and dock widgets""" menu = super(Workbench, self).createPopupMenu() assert menu # should have toolbar/dock menu # replace default log dock action by customized one menu.insertAction(self._console.toggleViewAction(), self._actionShowConsole) menu.removeAction(self._console.toggleViewAction()) menu.addSeparator() menu.addAction(self._actionDockedConsole) menu.addAction(_('Custom Toolbar &Settings'), self._editCustomToolsSettings) return menu @pyqtSlot(QAction) def _onSwitchRepoTaskTab(self, action): rw = self._currentRepoWidget() if rw: rw.switchToNamedTaskTab(str(action.data().toString())) @pyqtSlot(bool) def _setConsoleVisible(self, visible): if self._actionDockedConsole.isChecked(): self._setDockedConsoleVisible(visible) else: self._setConsoleTaskTabVisible(visible) def _setDockedConsoleVisible(self, visible): self._console.setVisible(visible) if visible: # not hook setVisible() or showEvent() in order to move focus # only when console is activated by user action self._console.setFocus() def _setConsoleTaskTabVisible(self, visible): rw = self._currentRepoWidget() if not rw: return if visible: rw.switchToNamedTaskTab('console') else: # it'll be better if it can switch to the last tab rw.switchToPreferredTaskTab() @pyqtSlot() def _updateShowConsoleAction(self): if self._actionDockedConsole.isChecked(): visible = self._console.isVisibleTo(self) enabled = True else: rw = self._currentRepoWidget() visible = bool(rw and rw.currentTaskTabName() == 'console') enabled = bool(rw) self._actionShowConsole.setChecked(visible) self._actionShowConsole.setEnabled(enabled) @pyqtSlot() def _updateDockedConsoleMode(self): docked = self._actionDockedConsole.isChecked() visible = self._actionShowConsole.isChecked() self._console.setVisible(docked and visible) self._setConsoleTaskTabVisible(not docked and visible) self._updateShowConsoleAction() @pyqtSlot(str, bool) def openRepo(self, root, reuse, bundle=None): """Open tab of the specified repo [unicode]""" root = unicode(root) if not root or root.startswith('ssh://'): return if reuse and self.repoTabsWidget.selectRepo(root): return if not self.repoTabsWidget.openRepo(root, bundle): return @pyqtSlot(str) def showRepo(self, root): """Activate the repo tab or open it if not available [unicode]""" self.openRepo(root, True) @pyqtSlot(str, str) def setRevsetFilter(self, path, filter): if self.repoTabsWidget.selectRepo(path): w = self.repoTabsWidget.currentWidget() w.setFilter(filter) def dragEnterEvent(self, event): d = event.mimeData() for u in d.urls(): root = paths.find_root(unicode(u.toLocalFile())) if root: event.setDropAction(Qt.LinkAction) event.accept() break def dropEvent(self, event): accept = False d = event.mimeData() for u in d.urls(): root = paths.find_root(unicode(u.toLocalFile())) if root: self.showRepo(root) accept = True if accept: event.setDropAction(Qt.LinkAction) event.accept() def _updateMenu(self): """Enable actions when repoTabs are opened or closed or changed""" # Update actions affected by repo open/close someRepoOpen = bool(self._currentRepoWidget()) for action in self._actionavails['repoopen']: action.setEnabled(someRepoOpen) for action in self._actionvisibles['repoopen']: action.setVisible(someRepoOpen) # Update actions affected by repo open/close/change self._updateTaskViewMenu() self._updateToolBarActions() @pyqtSlot() def _updateWindowTitle(self): w = self._currentRepoWidget() if not w: self.setWindowTitle(_('TortoiseHg Workbench')) elif w.repo.ui.configbool('tortoisehg', 'fullpath'): self.setWindowTitle(_('%s - TortoiseHg Workbench - %s') % (w.title(), w.repoRootPath())) else: self.setWindowTitle(_('%s - TortoiseHg Workbench') % w.title()) @pyqtSlot() def _updateToolBarActions(self): w = self._currentRepoWidget() if w: self.filtertbaction.setChecked(w.filterBarVisible()) @pyqtSlot() def _updateTaskViewMenu(self): 'Update task tab menu for current repository' repoWidget = self._currentRepoWidget() if not repoWidget: for a in self.actionGroupTaskView.actions(): a.setChecked(False) if self.actionSelectTaskPbranch is not None: self.actionSelectTaskPbranch.setVisible(False) self.lockToolAction.setVisible(False) else: exts = repoWidget.repo.extensions() if self.actionSelectTaskPbranch is not None: self.actionSelectTaskPbranch.setVisible('pbranch' in exts) name = repoWidget.currentTaskTabName() for action in self.actionGroupTaskView.actions(): action.setChecked(str(action.data().toString()) == name) self.lockToolAction.setVisible('simplelock' in exts) self._updateShowConsoleAction() for i, a in enumerate(a for a in self.actionGroupTaskView.actions() if a.isVisible()): a.setShortcut('Alt+%d' % (i + 1)) @pyqtSlot() def _updateHistoryActions(self): 'Update back / forward actions' rw = self._currentRepoWidget() self.actionBack.setEnabled(bool(rw and rw.canGoBack())) self.actionForward.setEnabled(bool(rw and rw.canGoForward())) @pyqtSlot() def repoTabChanged(self): self._updateHistoryActions() self._updateMenu() self._updateWindowTitle() @pyqtSlot(str) def _onCurrentRepoChanged(self, curpath): curpath = unicode(curpath) self._console.setCurrentRepoRoot(curpath or None) self.reporegistry.setActiveTabRepo(curpath) if curpath: repoagent = self._repomanager.repoAgent(curpath) repo = repoagent.rawRepo() self.mqpatches.setRepoAgent(repoagent) self._setupCustomTools(repo.ui) self._setupUrlCombo(repo) self._updateAbortAction(repoagent) else: self.mqpatches.setRepoAgent(None) self.actionAbort.setEnabled(False) @pyqtSlot() def _setHistoryColumns(self): """Display the column selection dialog""" w = self._currentRepoWidget() assert w w.repoview.setHistoryColumns() def _repotogglefwd(self, name): """Return function to forward action to the current repo tab""" def forwarder(checked): w = self._currentRepoWidget() if w: getattr(w, name)(checked) return forwarder def _repofwd(self, name, params=[], namedparams={}): """Return function to forward action to the current repo tab""" def forwarder(): w = self._currentRepoWidget() if w: getattr(w, name)(*params, **namedparams) return forwarder @pyqtSlot() def refresh(self): clear = QApplication.keyboardModifiers() & Qt.ControlModifier w = self._currentRepoWidget() if w: # check unnoticed changes to emit corresponding signals repoagent = self._repomanager.repoAgent(w.repoRootPath()) if clear: repoagent.clearStatus() repoagent.pollStatus() # TODO if all objects are responsive to repository signals, some # of the following actions are not necessary w.reload() @pyqtSlot(QAction) def _runSyncAction(self, action): w = self._currentRepoWidget() if w: op = str(action.data().toString()) w.setSyncUrl(self._syncUrlFor(op) or '') getattr(w, op)() @pyqtSlot() def _runSyncBookmarks(self): w = self._currentRepoWidget() if w: # the sync bookmark dialog is bidirectional but is only able to # handle one remote location therefore we use the push location w.setSyncUrl(self._syncUrlFor('push') or '') w.syncBookmark() @pyqtSlot() def _abortCommands(self): root = self.currentRepoRootPath() if not root: return repoagent = self._repomanager.repoAgent(root) repoagent.abortCommands() def _updateAbortAction(self, repoagent): self.actionAbort.setEnabled(repoagent.isBusy()) @pyqtSlot(str) def _onBusyChanged(self, root): repoagent = self._repomanager.repoAgent(root) self._updateAbortAction(repoagent) if not repoagent.isBusy(): self.statusbar.clearRepoProgress(root) self.statusbar.setRepoBusy(root, repoagent.isBusy()) def serve(self): self._dialogs.open(Workbench._createServeDialog) def _createServeDialog(self): w = self._currentRepoWidget() if w: return serve.run(w.repo.ui, root=w.repo.root) else: return serve.run(self.ui) def loadall(self): w = self._currentRepoWidget() if w: w.repoview.model().loadall() def _gotorev(self): rev, ok = qtlib.getTextInput(self, _("Goto revision"), _("Enter revision identifier")) if ok: self.gotorev(rev) @pyqtSlot(str) def gotorev(self, rev): w = self._currentRepoWidget() if w: w.repoview.goto(rev) def newWorkbench(self): cmdline = list(paths.get_thg_command()) cmdline.extend(['workbench', '--nofork', '--newworkbench']) subprocess.Popen(cmdline, creationflags=qtlib.openflags) def newRepository(self): """ Run init dialog """ from tortoisehg.hgqt.hginit import InitDialog path = self.currentRepoRootPath() or '.' dlg = InitDialog(self.ui, path, self) if dlg.exec_() == 0: self.openRepo(dlg.destination(), False) @pyqtSlot() @pyqtSlot(str) def cloneRepository(self, uroot=None): """ Run clone dialog """ # it might be better to reuse existing CloneDialog dlg = self._dialogs.openNew(Workbench._createCloneDialog) if not uroot: uroot = self.currentRepoRootPath() if uroot: dlg.setSource(uroot) dlg.setDestination(uroot + '-clone') def _createCloneDialog(self): from tortoisehg.hgqt.clone import CloneDialog dlg = CloneDialog(self.ui, parent=self) dlg.clonedRepository.connect(self._openClonedRepo) return dlg @pyqtSlot(str, str) def _openClonedRepo(self, root, sourceroot): root = unicode(root) sourceroot = unicode(sourceroot) self.reporegistry.addClonedRepo(root, sourceroot) self.showRepo(root) def openRepository(self): """ Open repo from File menu """ caption = _('Select repository directory to open') root = self.currentRepoRootPath() if root: cwd = os.path.dirname(root) else: cwd = os.getcwdu() FD = QFileDialog path = FD.getExistingDirectory(self, caption, cwd, FD.ShowDirsOnly | FD.ReadOnly) self.openRepo(path, False) def _currentRepoWidget(self): return self.repoTabsWidget.currentWidget() def currentRepoRootPath(self): return self.repoTabsWidget.currentRepoRootPath() def onAbout(self, *args): """ Display about dialog """ from tortoisehg.hgqt.about import AboutDialog ad = AboutDialog(self) ad.finished.connect(ad.deleteLater) ad.exec_() def onHelp(self, *args): """ Display online help """ qtlib.openhelpcontents('workbench.html') def onHelpExplorer(self, *args): """ Display online help for shell extension """ qtlib.openhelpcontents('explorer.html') def onReadme(self, *args): """Display the README file or URL for the current repo, or the global README if no repo is open""" readme = None def getCurrentReadme(repo): """ Get the README file that is configured for the current repo. README files can be set in 3 ways, which are checked in the following order of decreasing priority: - From the tortoisehg.readme key on the current repo's configuration file - An existing "README" file found on the repository root * Valid README files are those called README and whose extension is one of the following: ['', '.txt', '.html', '.pdf', '.doc', '.docx', '.ppt', '.pptx', '.markdown', '.textile', '.rdoc', '.org', '.creole', '.mediawiki','.rst', '.asciidoc', '.pod'] * Note that the match is CASE INSENSITIVE on ALL OSs. - From the tortoisehg.readme key on the user's global configuration file """ readme = None if repo: # Try to get the README configured for the repo of the current tab readmeglobal = self.ui.config('tortoisehg', 'readme', None) if readmeglobal: # Note that repo.ui.config() falls back to the self.ui.config() # if the key is not set on the current repo's configuration file readme = repo.ui.config('tortoisehg', 'readme', None) if readmeglobal != readme: # The readme is set on the current repo configuration file return readme # Otherwise try to see if there is a file at the root of the # repository that matches any of the valid README file names # (in a non case-sensitive way) # Note that we try to match the valid README names in order validreadmes = ['readme.txt', 'read.me', 'readme.html', 'readme.pdf', 'readme.doc', 'readme.docx', 'readme.ppt', 'readme.pptx', 'readme.md', 'readme.markdown', 'readme.mkdn', 'readme.rst', 'readme.textile', 'readme.rdoc', 'readme.asciidoc', 'readme.org', 'readme.creole', 'readme.mediawiki', 'readme.pod', 'readme'] readmefiles = [filename for filename in os.listdir(repo.root) if filename.lower().startswith('read')] for validname in validreadmes: for filename in readmefiles: if filename.lower() == validname: return repo.wjoin(filename) # Otherwise try use the global setting (or None if readme is just # not configured) return readmeglobal w = self._currentRepoWidget() if w: # Try to get the help doc from the current repo tap readme = getCurrentReadme(w.repo) if readme: qtlib.openlocalurl(os.path.expandvars(os.path.expandvars(readme))) else: qtlib.WarningMsgBox(_("README not configured"), _("A README file is not configured for the current repository.<p>" "To configure a README file for a repository, " "open the repository settings file, add a '<i>readme</i>' " "key to the '<i>tortoisehg</i>' section, and set it " "to the filename or URL of your repository's README file.")) def _storeSettings(self, repostosave, lastactiverepo): s = QSettings() wb = "Workbench/" s.setValue(wb + 'geometry', self.saveGeometry()) s.setValue(wb + 'windowState', self.saveState()) s.setValue(wb + 'dockedConsole', self._actionDockedConsole.isChecked()) s.setValue(wb + 'saveRepos', self.actionSaveRepos.isChecked()) s.setValue(wb + 'saveLastSyncPaths', self.actionSaveLastSyncPaths.isChecked()) s.setValue(wb + 'lastactiverepo', lastactiverepo) s.setValue(wb + 'openrepos', (',').join(repostosave)) s.beginWriteArray('lastreposyncpaths') lastreposyncpaths = {} if self.actionSaveLastSyncPaths.isChecked(): lastreposyncpaths = self._lastRepoSyncPath for n, root in enumerate(sorted(lastreposyncpaths)): s.setArrayIndex(n) s.setValue('root', root) s.setValue('path', self._lastRepoSyncPath[root]) s.endArray() def restoreSettings(self): s = QSettings() wb = "Workbench/" self.restoreGeometry(s.value(wb + 'geometry').toByteArray()) self.restoreState(s.value(wb + 'windowState').toByteArray()) self._actionDockedConsole.setChecked( s.value(wb + 'dockedConsole', True).toBool()) lastreposyncpaths = {} npaths = s.beginReadArray('lastreposyncpaths') for n in range(npaths): s.setArrayIndex(n) root = unicode(s.value('root').toString()) lastreposyncpaths[root] = s.value('path').toString() s.endArray() self._lastRepoSyncPath = lastreposyncpaths save = s.value(wb + 'saveRepos').toBool() self.actionSaveRepos.setChecked(save) savelastsyncpaths = s.value(wb + 'saveLastSyncPaths').toBool() self.actionSaveLastSyncPaths.setChecked(savelastsyncpaths) openreposvalue = unicode(s.value(wb + 'openrepos').toString()) if openreposvalue: openrepos = openreposvalue.split(',') else: openrepos = [] # Note that if a "root" has been passed to the "thg" command, # "lastactiverepo" will have no effect lastactiverepo = unicode(s.value(wb + 'lastactiverepo').toString()) self.repoTabsWidget.restoreRepos(openrepos, lastactiverepo) # Clear the lastactiverepo and the openrepos list once the workbench state # has been reload, so that opening additional workbench windows does not # reopen these repos again s.setValue(wb + 'openrepos', '') s.setValue(wb + 'lastactiverepo', '') def goto(self, root, rev): if self.repoTabsWidget.selectRepo(hglib.tounicode(root)): rw = self.repoTabsWidget.currentWidget() rw.goto(rev) def closeEvent(self, event): repostosave = [] lastactiverepo = '' if self.actionSaveRepos.isChecked(): tw = self.repoTabsWidget repostosave = map(tw.repoRootPath, xrange(tw.count())) lastactiverepo = tw.currentRepoRootPath() if not self.repoTabsWidget.closeAllTabs(): event.ignore() else: self._storeSettings(repostosave, lastactiverepo) self.reporegistry.close() @pyqtSlot() def closeCurrentRepoTab(self): """close the current repo tab""" self.repoTabsWidget.closeTab(self.repoTabsWidget.currentIndex()) def explore(self): root = self.currentRepoRootPath() if root: qtlib.openlocalurl(hglib.fromunicode(root)) def terminal(self): w = self._currentRepoWidget() if w: qtlib.openshell(w.repo.root, hglib.fromunicode(w.repoDisplayName()), w.repo.ui) @pyqtSlot() def editSettings(self, focus=None): sd = SettingsDialog(configrepo=False, focus=focus, parent=self, root=hglib.fromunicode(self.currentRepoRootPath())) sd.exec_() @pyqtSlot() def _editCustomToolsSettings(self): self.editSettings('tools')