1# workbench.py - main TortoiseHg Window
2#
3# Copyright (C) 2007-2010 Logilab. All rights reserved.
4#
5# This software may be used and distributed according to the terms
6# of the GNU General Public License, incorporated herein by reference.
7"""
8Main Qt4 application for TortoiseHg
9"""
10
11from __future__ import absolute_import
12
13import os
14import subprocess
15import sys
16
17from .qtcore import (
18    QSettings,
19    Qt,
20    pyqtSlot,
21)
22from .qtgui import (
23    QAction,
24    QActionGroup,
25    QApplication,
26    QComboBox,
27    QFileDialog,
28    QKeySequence,
29    QMainWindow,
30    QMenu,
31    QMenuBar,
32    QShortcut,
33    QSizePolicy,
34    QToolBar,
35    qApp,
36)
37
38from mercurial import (
39    pycompat,
40)
41
42from ..util import (
43    hglib,
44    paths,
45)
46from ..util.i18n import _
47from . import (
48    cmdcore,
49    cmdui,
50    mq,
51    qtlib,
52    repotab,
53    serve,
54    shortcutsettings,
55)
56from .docklog import LogDockWidget
57from .reporegistry import RepoRegistryView
58from .settings import SettingsDialog
59
60class Workbench(QMainWindow):
61    """hg repository viewer/browser application"""
62
63    def __init__(self, ui, config, actionregistry, repomanager):
64        QMainWindow.__init__(self)
65        self.ui = ui
66        self._config = config
67        self._actionregistry = actionregistry
68        self._repomanager = repomanager
69        self._repomanager.configChanged.connect(self._setupUrlComboIfCurrent)
70
71        self.setupUi()
72        repomanager.busyChanged.connect(self._onBusyChanged)
73        repomanager.progressReceived.connect(self.statusbar.setRepoProgress)
74
75        self.reporegistry = rr = RepoRegistryView(repomanager, self)
76        rr.setObjectName('RepoRegistryView')
77        rr.showMessage.connect(self.statusbar.showMessage)
78        rr.openRepo.connect(self.openRepo)
79        rr.removeRepo.connect(self.repoTabsWidget.closeRepo)
80        rr.cloneRepoRequested.connect(self.cloneRepository)
81        rr.progressReceived.connect(self.statusbar.progress)
82        self._repomanager.repositoryChanged.connect(rr.scanRepo)
83        rr.hide()
84        self.addDockWidget(Qt.LeftDockWidgetArea, rr)
85
86        self.mqpatches = p = mq.MQPatchesWidget(actionregistry, self)
87        p.setObjectName('MQPatchesWidget')
88        p.patchSelected.connect(self.gotorev)
89        p.hide()
90        self.addDockWidget(Qt.LeftDockWidgetArea, p)
91
92        cmdagent = cmdcore.CmdAgent(ui, self)
93        self._console = LogDockWidget(repomanager, cmdagent, self)
94        self._console.setObjectName('Log')
95        self._console.hide()
96        self._console.visibilityChanged.connect(self._updateShowConsoleAction)
97        self.addDockWidget(Qt.BottomDockWidgetArea, self._console)
98
99        self._setupActions()
100
101        self.restoreSettings()
102        self.repoTabChanged()
103        self.setAcceptDrops(True)
104        self.setIconSize(qtlib.toolBarIconSize())
105        if os.name == 'nt':
106            # Allow CTRL+Q to close Workbench on Windows
107            QShortcut(QKeySequence('CTRL+Q'), self, self.close)
108        if sys.platform == 'darwin':
109            self.dockMenu = QMenu(self)
110            self.dockMenu.addAction(_('New &Workbench'),
111                                    self.newWorkbench)
112            self.dockMenu.addAction(_('&New Repository...'),
113                                    self.newRepository)
114            self.dockMenu.addAction(_('Clon&e Repository...'),
115                                    self.cloneRepository)
116            self.dockMenu.addAction(_('&Open Repository...'),
117                                    self.openRepository)
118            self.dockMenu.setAsDockMenu()
119
120        self._dialogs = qtlib.DialogKeeper(
121            lambda self, dlgmeth: dlgmeth(self), parent=self)
122
123    def setupUi(self):
124        desktopgeom = qApp.desktop().availableGeometry()
125        self.resize(desktopgeom.size() * 0.8)
126
127        self.repoTabsWidget = tw = repotab.RepoTabWidget(
128            self._config, self._actionregistry, self._repomanager, self)
129        sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
130        sp.setHorizontalStretch(1)
131        sp.setVerticalStretch(1)
132        sp.setHeightForWidth(tw.sizePolicy().hasHeightForWidth())
133        tw.setSizePolicy(sp)
134        tw.currentTabChanged.connect(self.repoTabChanged)
135        tw.currentRepoChanged.connect(self._onCurrentRepoChanged)
136        tw.currentTaskTabChanged.connect(self._updateTaskViewMenu)
137        tw.currentTitleChanged.connect(self._updateWindowTitle)
138        tw.historyChanged.connect(self._updateHistoryActions)
139        tw.makeLogVisible.connect(self._setConsoleVisible)
140        tw.taskTabVisibilityChanged.connect(self._updateTaskTabVisibilityAction)
141        tw.toolbarVisibilityChanged.connect(self._updateToolBarActions)
142
143        self.setCentralWidget(tw)
144        self.statusbar = cmdui.ThgStatusBar(self)
145        self.setStatusBar(self.statusbar)
146
147        tw.progressReceived.connect(self.statusbar.setRepoProgress)
148        tw.showMessageSignal.connect(self.statusbar.showMessage)
149
150    def _setupActions(self):
151        """Setup actions, menus and toolbars"""
152        self.menubar = QMenuBar(self)
153        self.setMenuBar(self.menubar)
154
155        self.menuFile = self.menubar.addMenu(_("&File"))
156        self.menuView = self.menubar.addMenu(_("&View"))
157        self.menuRepository = self.menubar.addMenu(_("&Repository"))
158        self.menuHelp = self.menubar.addMenu(_("&Help"))
159
160        self.edittbar = QToolBar(_("&Edit Toolbar"), objectName='edittbar')
161        self.addToolBar(self.edittbar)
162        self.docktbar = QToolBar(_("&Dock Toolbar"), objectName='docktbar')
163        self.addToolBar(self.docktbar)
164        self.tasktbar = QToolBar(_('&Task Toolbar'), objectName='taskbar')
165        self.addToolBar(self.tasktbar)
166        self.customtbar = QToolBar(_('&Custom Toolbar'), objectName='custombar')
167        self.addToolBar(self.customtbar)
168        self.synctbar = QToolBar(_('S&ync Toolbar'), objectName='synctbar')
169        self.addToolBar(self.synctbar)
170
171        # availability map of actions; applied by _updateMenu()
172        self._actionavails = {'repoopen': []}
173        self._actionvisibles = {'repoopen': []}
174
175        newaction = self._addNewAction
176        newnamed = self._addNewNamedAction
177        newseparator = self._addNewSeparator
178
179        newnamed('Workbench.newWorkbench', self.newWorkbench,
180                 menu='file', icon='hg-log')
181        newseparator(menu='file')
182        newnamed('Workbench.newRepository', self.newRepository,
183                 menu='file', icon='hg-init')
184        newnamed('Workbench.cloneRepository', self.cloneRepository,
185                 menu='file', icon='hg-clone')
186        newseparator(menu='file')
187        newnamed('Workbench.openRepository', self.openRepository, menu='file')
188        newnamed('Workbench.closeRepository', self.closeCurrentRepoTab,
189                 enabled='repoopen', menu='file')
190        newseparator(menu='file')
191        self.menuFile.addActions(self.repoTabsWidget.tabSwitchActions())
192        newseparator(menu='file')
193        newnamed('Workbench.openSettings', self.editSettings,
194                 icon='thg-userconfig', menu='file')
195        newnamed('Workbench.openShortcutSettings',
196                 self._openShortcutSettingsDialog, menu='file')
197        newseparator(menu='file')
198        newnamed('Workbench.quit', self.close, menu='file')
199
200        a = self.reporegistry.toggleViewAction()
201        a.setIcon(qtlib.geticon('thg-reporegistry'))
202        self._actionregistry.registerAction('Workbench.showRepoRegistry', a)
203        self.docktbar.addAction(a)
204        self.menuView.addAction(a)
205
206        a = self.mqpatches.toggleViewAction()
207        a.setIcon(qtlib.geticon('thg-mq'))
208        self._actionregistry.registerAction('Workbench.showPatchQueue', a)
209        self.docktbar.addAction(a)
210        self.menuView.addAction(a)
211
212        self._actionShowConsole = a = QAction(self)
213        a.setCheckable(True)
214        a.setIcon(qtlib.geticon('thg-console'))
215        a.triggered.connect(self._setConsoleVisible)
216        self._actionregistry.registerAction('Workbench.showConsole', a)
217        self.docktbar.addAction(a)
218        self.menuView.addAction(a)
219
220        self._actionDockedConsole = a = QAction(self)
221        a.setText(_('Place Console in Doc&k Area'))
222        a.setCheckable(True)
223        a.setChecked(True)
224        a.triggered.connect(self._updateDockedConsoleMode)
225
226        newseparator(menu='view')
227        menu = self.menuView.addMenu(_('R&epository Registry Options'))
228        menu.addActions(self.reporegistry.settingActions())
229
230        newseparator(menu='view')
231        newnamed('RepoView.setHistoryColumns', self._setHistoryColumns,
232                 enabled='repoopen', menu='view')
233        self.actionSaveRepos = \
234        newaction(_("Save Open Repositories on E&xit"), checkable=True,
235                  menu='view')
236        self.actionSaveLastSyncPaths = \
237        newaction(_("Sa&ve Current Sync Paths on Exit"), checkable=True,
238                  menu='view')
239        newseparator(menu='view')
240
241        a = newaction(_('Show Tas&k Tab'), shortcut='Alt+0', checkable=True,
242                      enabled='repoopen', menu='view')
243        a.triggered.connect(self._setRepoTaskTabVisible)
244        self.actionTaskTabVisible = a
245
246        self.actionGroupTaskView = QActionGroup(self)
247        self.actionGroupTaskView.triggered.connect(self._onSwitchRepoTaskTab)
248        def addtaskview(icon, label, name):
249            a = newaction(label, icon=None, checkable=True, data=name,
250                          enabled='repoopen', menu='view')
251            a.setIcon(qtlib.geticon(icon))
252            self.actionGroupTaskView.addAction(a)
253            self.tasktbar.addAction(a)
254            return a
255
256        # note that 'grep' and 'search' are equivalent
257        taskdefs = {
258            'commit': ('hg-commit', _('&Commit')),
259            'log': ('hg-log', _("Revision &Details")),
260            'grep': ('hg-grep', _('&Search')),
261            'sync': ('thg-sync', _('S&ynchronize')),
262            # 'console' is toggled by "Show Console" action
263        }
264        tasklist = self._config.configStringList(
265            'tortoisehg', 'workbench.task-toolbar')
266        if tasklist == []:
267            tasklist = ['log', 'commit', 'grep', '|', 'sync']
268
269        for taskname in tasklist:
270            taskname = taskname.strip()
271            taskinfo = taskdefs.get(taskname, None)
272            if taskinfo is None:
273                newseparator(toolbar='task')
274                continue
275            addtaskview(taskinfo[0], taskinfo[1], taskname)
276
277        newseparator(menu='view')
278
279        newnamed('Workbench.refresh', self.refresh, icon='view-refresh',
280                 enabled='repoopen', menu='view', toolbar='edit',
281                 tooltip=_('Refresh current repository'))
282        newnamed('Workbench.refreshTaskTabs', self._repofwd('reloadTaskTab'),
283                 enabled='repoopen',
284                 tooltip=_('Refresh only the current task tab'),
285                 menu='view')
286        newnamed('RepoView.loadAllRevisions', self.loadall,
287                 enabled='repoopen', menu='view',
288                 tooltip=_('Load all revisions into graph'))
289
290        self.actionAbort = newnamed('Workbench.abort', self._abortCommands,
291                                    icon='process-stop',
292                                    toolbar='edit',
293                                    tooltip=_('Stop current operation'))
294        self.actionAbort.setEnabled(False)
295
296        newseparator(toolbar='edit')
297        newnamed('RepoView.goToWorkingParent', self._repofwd('gotoParent'),
298                 icon='go-home', tooltip=_('Go to current revision'),
299                 enabled='repoopen', toolbar='edit')
300        newnamed('RepoView.goToRevision', self._gotorev, icon='go-to-rev',
301                 tooltip=_('Go to a specific revision'),
302                 enabled='repoopen', menu='view', toolbar='edit')
303
304        self.actionBack = newnamed('RepoView.goBack', self._repofwd('back'),
305                                   icon='go-previous',
306                                   enabled=False, toolbar='edit')
307        self.actionForward = newnamed('RepoView.goForward',
308                                      self._repofwd('forward'), icon='go-next',
309                                      enabled=False, toolbar='edit')
310        newseparator(toolbar='edit', menu='View')
311
312        self.filtertbaction = newnamed('RepoView.showFilterBar',
313                                       self._repotogglefwd('toggleFilterBar'),
314                                       icon='view-filter', enabled='repoopen',
315                                       toolbar='edit', menu='View',
316                                       checkable=True,
317                                       tooltip=_('Filter graph with revision '
318                                                 'sets or branches'))
319
320        menu = QMenu(_('&Workbench Toolbars'), self)
321        menu.addAction(self.edittbar.toggleViewAction())
322        menu.addAction(self.docktbar.toggleViewAction())
323        menu.addAction(self.tasktbar.toggleViewAction())
324        menu.addAction(self.synctbar.toggleViewAction())
325        menu.addAction(self.customtbar.toggleViewAction())
326        self.menuView.addMenu(menu)
327
328        newseparator(toolbar='edit')
329        menuSync = self.menuRepository.addMenu(_('S&ynchronize'))
330        a = newnamed('Repository.lockFile', self._repofwd('lockTool'),
331                     icon='thg-password', enabled='repoopen',
332                     menu='repository', toolbar='edit',
333                     tooltip=_('Lock or unlock files'))
334        self.lockToolAction = a
335        newseparator(menu='repository')
336        newnamed('Repository.update', self._repofwd('updateToRevision'),
337                 icon='hg-update', enabled='repoopen',
338                 menu='repository', toolbar='edit',
339                 tooltip=_('Update working directory or switch revisions'))
340        newnamed('Repository.shelve', self._repofwd('shelve'), icon='hg-shelve',
341                 enabled='repoopen', menu='repository')
342        newnamed('Repository.import', self._repofwd('thgimport'),
343                 icon='hg-import', enabled='repoopen', menu='repository')
344        newnamed('Repository.unbundle', self._repofwd('unbundle'),
345                 icon='hg-unbundle', enabled='repoopen', menu='repository')
346        newseparator(menu='repository')
347        newnamed('Repository.merge', self._repofwd('mergeWithOtherHead'),
348                 icon='hg-merge', enabled='repoopen',
349                 menu='repository', toolbar='edit',
350                 tooltip=_('Merge with the other head of the current branch'))
351        newnamed('Repository.resolve', self._repofwd('resolve'),
352                 enabled='repoopen', menu='repository')
353        newseparator(menu='repository')
354        newnamed('Repository.rollback', self._repofwd('rollback'),
355                 enabled='repoopen', menu='repository')
356        newseparator(menu='repository')
357        newnamed('Repository.purge', self._repofwd('purge'), enabled='repoopen',
358                 icon='hg-purge', menu='repository')
359        newseparator(menu='repository')
360        newnamed('Repository.bisect', self._repofwd('bisect'),
361                 enabled='repoopen', menu='repository')
362        newseparator(menu='repository')
363        newnamed('Repository.verify', self._repofwd('verify'),
364                 enabled='repoopen', menu='repository')
365        newnamed('Repository.recover', self._repofwd('recover'),
366                 enabled='repoopen', menu='repository')
367        newseparator(menu='repository')
368        newnamed('Workbench.openFileManager', self.explore,
369                 icon='system-file-manager', enabled='repoopen',
370                 menu='repository')
371        newnamed('Workbench.openTerminal', self.terminal,
372                 icon='utilities-terminal', enabled='repoopen',
373                 menu='repository')
374        newnamed('Workbench.webServer', self.serve, menu='repository',
375                 icon='hg-serve')
376
377        newnamed('Workbench.help', self.onHelp, menu='help',
378                 icon='help-browser')
379        newnamed('Workbench.explorerHelp', self.onHelpExplorer, menu='help')
380        visiblereadme = 'repoopen'
381        if self._config.configString('tortoisehg', 'readme'):
382            visiblereadme = True
383        newnamed('Workbench.openReadme', self.onReadme, menu='help',
384                 icon='help-readme', visible=visiblereadme)
385        newseparator(menu='help')
386        newnamed('Workbench.aboutQt', QApplication.aboutQt, menu='help')
387        newnamed('Workbench.about', self.onAbout, menu='help', icon='thg')
388
389        syncActionGroup = QActionGroup(self)
390        syncActionGroup.triggered.connect(self._runSyncAction)
391        newnamed('Repository.incoming', data='incoming', icon='hg-incoming',
392                 enabled='repoopen', toolbar='sync', group=syncActionGroup)
393        pullAction = newnamed('Repository.pull', data='pull', icon='hg-pull',
394                              enabled='repoopen', toolbar='sync',
395                              group=syncActionGroup)
396        newnamed('Repository.outgoing', data='outgoing', icon='hg-outgoing',
397                 enabled='repoopen', toolbar='sync', group=syncActionGroup)
398        pushAction = newnamed('Repository.push', data='push', icon='hg-push',
399                              enabled='repoopen', toolbar='sync',
400                              group=syncActionGroup)
401        menuSync.addActions(syncActionGroup.actions())
402        menuSync.addSeparator()
403
404        def addSyncActionMenu(parentAction, action):
405            tbb = self.synctbar.widgetForAction(parentAction)
406            menu = QMenu(self)
407            menu.addAction(action)
408            tbb.setMenu(menu)
409        syncAllTabsActionGroup = QActionGroup(self)
410        syncAllTabsActionGroup.triggered.connect(self._runSyncAllTabsAction)
411        addSyncActionMenu(pullAction,
412                          newnamed('Repository.pullAllTabs',
413                                   data='pull', icon='hg-pull',
414                                   enabled='repoopen',
415                                   group=syncAllTabsActionGroup))
416        addSyncActionMenu(pushAction,
417                          newnamed('Repository.pushAllTabs',
418                                   data='push', icon='hg-push',
419                                   enabled='repoopen',
420                                   group=syncAllTabsActionGroup))
421        menuSync.addActions(syncAllTabsActionGroup.actions())
422        menuSync.addSeparator()
423
424        action = QAction(self)
425        action.setIcon(qtlib.geticon('thg-sync-bookmarks'))
426        self._actionavails['repoopen'].append(action)
427        action.triggered.connect(self._runSyncBookmarks)
428        self._actionregistry.registerAction('Repository.syncBookmarks', action)
429        menuSync.addAction(action)
430
431        self._lastRepoSyncPath = {}
432        self.urlCombo = QComboBox(self)
433        self.urlCombo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
434        self.urlCombo.currentIndexChanged.connect(self._updateSyncUrl)
435        self.urlComboAction = self.synctbar.addWidget(self.urlCombo)
436        # hide it because workbench could be started without open repo
437        self.urlComboAction.setVisible(False)
438
439    def _setupUrlCombo(self, repoagent):
440        """repository has been switched, fill urlCombo with URLs"""
441        pathdict = dict(repoagent.configStringItems('paths'))
442        aliases = list(pathdict.keys())
443
444        combo_setting = repoagent.configString(
445            'tortoisehg', 'workbench.target-combo')
446        self.urlComboAction.setVisible(len(aliases) > 1
447                                       or combo_setting == 'always')
448
449        # 1. Sort the list if aliases
450        aliases.sort()
451        # 2. Place the default alias at the top of the list
452        if 'default' in aliases:
453            aliases.remove('default')
454            aliases.insert(0, 'default')
455        # 3. Make a list of paths that have a 'push path'
456        # note that the default path will be first (if it has a push path),
457        # followed by the other paths that have a push path, alphabetically
458        haspushaliases = [alias for alias in aliases
459                         if alias + '-push' in aliases]
460        # 4. Place the "-push" paths next to their "pull paths"
461        regularaliases = []
462        for a in aliases[:]:
463            if a.endswith('-push'):
464                if a[:-len('-push')] in haspushaliases:
465                    continue
466            regularaliases.append(a)
467            if a in haspushaliases:
468                regularaliases.append(a + '-push')
469        # 5. Create the list of 'combined aliases'
470        combinedaliases = [(a, a + '-push') for a in haspushaliases]
471        # 6. Put the combined aliases first, followed by the regular aliases
472        aliases = combinedaliases + regularaliases
473        # 7. Ensure the first path is a default path (either a
474        # combined "default | default-push" path or a regular default path)
475        if 'default-push' not in aliases and 'default' in aliases:
476            aliases.remove('default')
477            aliases.insert(0, 'default')
478
479        self.urlCombo.blockSignals(True)
480        self.urlCombo.clear()
481        for n, a in enumerate(aliases):
482            # text, (pull-alias, push-alias)
483            if isinstance(a, tuple):
484                itemtext = u'\u2193 %s | %s \u2191' % a
485                itemdata = tuple(pathdict[alias] for alias in a)
486                tooltip = _('pull: %s\npush: %s') % itemdata
487            else:
488                itemtext = a
489                itemdata = (pathdict[a], pathdict[a])
490                tooltip = pathdict[a]
491            self.urlCombo.addItem(itemtext, itemdata)
492            self.urlCombo.setItemData(n, tooltip, Qt.ToolTipRole)
493        # Try to select the previously selected path, if any
494        prevpath = self._lastRepoSyncPath.get(repoagent.rootPath())
495        if prevpath:
496            idx = self.urlCombo.findText(prevpath)
497            if idx >= 0:
498                self.urlCombo.setCurrentIndex(idx)
499        self.urlCombo.blockSignals(False)
500        self._updateSyncUrlToolTip(self.urlCombo.currentIndex())
501
502    @pyqtSlot(str)
503    def _setupUrlComboIfCurrent(self, root):
504        w = self._currentRepoWidget()
505        if w.repoRootPath() == root:
506            self._setupUrlCombo(self._repomanager.repoAgent(root))
507
508    def _syncUrlFor(self, op):
509        """Current URL for the given sync operation"""
510        urlindex = self.urlCombo.currentIndex()
511        if urlindex < 0:
512            return
513        opindex = {'incoming': 0, 'pull': 0, 'outgoing': 1, 'push': 1}[op]
514        return self.urlCombo.itemData(urlindex)[opindex]
515
516    @pyqtSlot(int)
517    def _updateSyncUrl(self, index):
518        self._updateSyncUrlToolTip(index)
519        # save the new url for later recovery
520        reporoot = self.currentRepoRootPath()
521        if not reporoot:
522            return
523        path = self.urlCombo.currentText()
524        self._lastRepoSyncPath[reporoot] = path
525
526    def _updateSyncUrlToolTip(self, index):
527        self._updateUrlComboToolTip(index)
528        self._updateSyncActionToolTip(index)
529
530    def _updateUrlComboToolTip(self, index):
531        if not self.urlCombo.count():
532            tooltip = _('There are no configured sync paths.\n'
533                        'Open the Synchronize tab to configure them.')
534        else:
535            tooltip = self.urlCombo.itemData(index, Qt.ToolTipRole)
536        self.urlCombo.setToolTip(tooltip)
537
538    def _updateSyncActionToolTip(self, index):
539        if index < 0:
540            tooltips = {
541                'incoming': _('Check for incoming changes'),
542                'pull':     _('Pull incoming changes'),
543                'outgoing': _('Detect outgoing changes'),
544                'push':     _('Push outgoing changes'),
545                }
546        else:
547            pullurl, pushurl = self.urlCombo.itemData(index)
548            tooltips = {
549                'incoming': _('Check for incoming changes from\n%s') % pullurl,
550                'pull':     _('Pull incoming changes from\n%s') % pullurl,
551                'outgoing': _('Detect outgoing changes to\n%s') % pushurl,
552                'push':     _('Push outgoing changes to\n%s') % pushurl,
553                }
554
555        for a in self.synctbar.actions():
556            op = str(a.data())
557            if op in tooltips:
558                a.setToolTip(tooltips[op])
559
560    def _setupCustomTools(self, ui):
561        tools, toollist = hglib.tortoisehgtools(ui,
562            selectedlocation='workbench.custom-toolbar')
563        # Clear the existing "custom" toolbar
564        self.customtbar.clear()
565        # and repopulate it again with the tool configuration
566        # for the current repository
567        if not tools:
568            return
569        for name in toollist:
570            if name == '|':
571                self._addNewSeparator(toolbar='custom')
572                continue
573            info = tools.get(name, None)
574            if info is None:
575                continue
576            command = info.get('command', None)
577            if not command:
578                continue
579            showoutput = info.get('showoutput', False)
580            workingdir = info.get('workingdir', '')
581            label = info.get('label', name)
582            tooltip = info.get('tooltip', _("Execute custom tool '%s'") % label)
583            icon = info.get('icon', 'tools-spanner-hammer')
584
585            self._addNewAction(label,
586                self._repofwd('runCustomCommand',
587                              [command, showoutput, workingdir]),
588                icon=icon, tooltip=tooltip,
589                enabled=True, toolbar='custom')
590
591    def _addNewAction(self, text, slot=None, icon=None, shortcut=None,
592                  checkable=False, tooltip=None, data=None, enabled=None,
593                  visible=None, menu=None, toolbar=None, group=None):
594        """Create new action and register it
595
596        :slot: function called if action triggered or toggled.
597        :checkable: checkable action. slot will be called on toggled.
598        :data: optional data stored on QAction.
599        :enabled: bool or group name to enable/disable action.
600        :visible: bool or group name to show/hide action.
601        :shortcut: QKeySequence, key sequence or name of standard key.
602        :menu: name of menu to add this action.
603        :toolbar: name of toolbar to add this action.
604        """
605        action = QAction(text, self, checkable=checkable)
606        if slot:
607            if checkable:
608                action.toggled.connect(slot)
609            else:
610                action.triggered.connect(slot)
611        if icon:
612            action.setIcon(qtlib.geticon(icon))
613        if shortcut:
614            keyseq = qtlib.keysequence(shortcut)
615            if isinstance(keyseq, QKeySequence.StandardKey):
616                action.setShortcuts(keyseq)
617            else:
618                action.setShortcut(keyseq)
619        if tooltip:
620            if action.shortcut():
621                tooltip += ' (%s)' % action.shortcut().toString()
622            action.setToolTip(tooltip)
623        if data is not None:
624            action.setData(data)
625        if isinstance(enabled, bool):
626            action.setEnabled(enabled)
627        elif enabled:
628            self._actionavails[enabled].append(action)
629        if isinstance(visible, bool):
630            action.setVisible(visible)
631        elif visible:
632            self._actionvisibles[visible].append(action)
633        if menu:
634            getattr(self, 'menu%s' % menu.title()).addAction(action)
635        if toolbar:
636            getattr(self, '%stbar' % toolbar).addAction(action)
637        if group:
638            group.addAction(action)
639        return action
640
641    def _addNewNamedAction(self, name, slot=None, icon=None, checkable=False,
642                           tooltip=None, data=None, enabled=None,
643                           visible=None, menu=None, toolbar=None, group=None):
644        """Create new action and register it as user-configurable"""
645        a = self._addNewAction('', slot=slot, icon=icon, checkable=checkable,
646                               tooltip=tooltip, data=data, enabled=enabled,
647                               visible=visible, menu=menu, toolbar=toolbar,
648                               group=group)
649        self._actionregistry.registerAction(name, a)
650        return a
651
652    def _addNewSeparator(self, menu=None, toolbar=None):
653        """Insert a separator action; returns nothing"""
654        if menu:
655            getattr(self, 'menu%s' % menu.title()).addSeparator()
656        if toolbar:
657            getattr(self, '%stbar' % toolbar).addSeparator()
658
659    def createPopupMenu(self):
660        """Create new popup menu for toolbars and dock widgets"""
661        menu = super(Workbench, self).createPopupMenu()
662        assert menu  # should have toolbar/dock menu
663        # replace default log dock action by customized one
664        menu.insertAction(self._console.toggleViewAction(),
665                          self._actionShowConsole)
666        menu.removeAction(self._console.toggleViewAction())
667        menu.addSeparator()
668        menu.addAction(self._actionDockedConsole)
669        menu.addAction(_('Custom Toolbar &Settings'),
670                       self._editCustomToolsSettings)
671        return menu
672
673    @pyqtSlot(QAction)
674    def _onSwitchRepoTaskTab(self, action):
675        rw = self._currentRepoWidget()
676        if rw:
677            rw.switchToNamedTaskTab(str(action.data()))
678
679    @pyqtSlot(bool)
680    def _setRepoTaskTabVisible(self, visible):
681        rw = self._currentRepoWidget()
682        if not rw:
683            return
684        rw.setTaskTabVisible(visible)
685
686    @pyqtSlot(bool)
687    def _setConsoleVisible(self, visible):
688        if self._actionDockedConsole.isChecked():
689            self._setDockedConsoleVisible(visible)
690        else:
691            self._setConsoleTaskTabVisible(visible)
692
693    def _setDockedConsoleVisible(self, visible):
694        self._console.setVisible(visible)
695        if visible:
696            # not hook setVisible() or showEvent() in order to move focus
697            # only when console is activated by user action
698            self._console.setFocus()
699
700    def _setConsoleTaskTabVisible(self, visible):
701        rw = self._currentRepoWidget()
702        if not rw:
703            return
704        if visible:
705            rw.switchToNamedTaskTab('console')
706        else:
707            # it'll be better if it can switch to the last tab
708            rw.switchToPreferredTaskTab()
709
710    @pyqtSlot()
711    def _updateShowConsoleAction(self):
712        if self._actionDockedConsole.isChecked():
713            visible = self._console.isVisibleTo(self)
714            enabled = True
715        else:
716            rw = self._currentRepoWidget()
717            visible = bool(rw and rw.currentTaskTabName() == 'console')
718            enabled = bool(rw)
719        self._actionShowConsole.setChecked(visible)
720        self._actionShowConsole.setEnabled(enabled)
721
722    @pyqtSlot()
723    def _updateDockedConsoleMode(self):
724        docked = self._actionDockedConsole.isChecked()
725        visible = self._actionShowConsole.isChecked()
726        self._console.setVisible(docked and visible)
727        self._setConsoleTaskTabVisible(not docked and visible)
728        self._updateShowConsoleAction()
729
730    @pyqtSlot(str, bool)
731    def openRepo(self, root, reuse, bundle=None):
732        """Open tab of the specified repo [unicode]"""
733        root = pycompat.unicode(root)
734        if not root or root.startswith('ssh://'):
735            return
736        if reuse and self.repoTabsWidget.selectRepo(root):
737            return
738        if not self.repoTabsWidget.openRepo(root, bundle):
739            return
740
741    @pyqtSlot(str)
742    def showRepo(self, root):
743        """Activate the repo tab or open it if not available [unicode]"""
744        self.openRepo(root, True)
745
746    @pyqtSlot(str, str)
747    def setRevsetFilter(self, path, filter):
748        if self.repoTabsWidget.selectRepo(path):
749            w = self.repoTabsWidget.currentWidget()
750            w.setFilter(filter)
751
752    def dragEnterEvent(self, event):
753        d = event.mimeData()
754        for u in d.urls():
755            root = paths.find_root(pycompat.unicode(u.toLocalFile()))
756            if root:
757                event.setDropAction(Qt.LinkAction)
758                event.accept()
759                break
760
761    def dropEvent(self, event):
762        accept = False
763        d = event.mimeData()
764        for u in d.urls():
765            root = paths.find_root(pycompat.unicode(u.toLocalFile()))
766            if root:
767                self.showRepo(root)
768                accept = True
769        if accept:
770            event.setDropAction(Qt.LinkAction)
771            event.accept()
772
773    def _updateMenu(self):
774        """Enable actions when repoTabs are opened or closed or changed"""
775
776        # Update actions affected by repo open/close
777        someRepoOpen = bool(self._currentRepoWidget())
778        for action in self._actionavails['repoopen']:
779            action.setEnabled(someRepoOpen)
780        for action in self._actionvisibles['repoopen']:
781            action.setVisible(someRepoOpen)
782
783        # Update actions affected by repo open/close/change
784        self._updateTaskViewMenu()
785        self._updateTaskTabVisibilityAction()
786        self._updateToolBarActions()
787
788    @pyqtSlot()
789    def _updateWindowTitle(self):
790        w = self._currentRepoWidget()
791        if not w:
792            self.setWindowTitle(_('TortoiseHg Workbench'))
793            return
794        repoagent = self._repomanager.repoAgent(w.repoRootPath())
795        if repoagent.configBool('tortoisehg', 'fullpath'):
796            self.setWindowTitle(_('%s - TortoiseHg Workbench - %s') %
797                                (w.title(), w.repoRootPath()))
798        else:
799            self.setWindowTitle(_('%s - TortoiseHg Workbench') % w.title())
800
801    @pyqtSlot()
802    def _updateToolBarActions(self):
803        w = self._currentRepoWidget()
804        if w:
805            self.filtertbaction.setChecked(w.filterBarVisible())
806
807    @pyqtSlot()
808    def _updateTaskViewMenu(self):
809        'Update task tab menu for current repository'
810        repoWidget = self._currentRepoWidget()
811        if not repoWidget:
812            for a in self.actionGroupTaskView.actions():
813                a.setChecked(False)
814            self.lockToolAction.setVisible(False)
815        else:
816            exts = repoWidget.repo.extensions()
817            name = repoWidget.currentTaskTabName()
818            for action in self.actionGroupTaskView.actions():
819                action.setChecked(str(action.data()) == name)
820            self.lockToolAction.setVisible('simplelock' in exts)
821        self._updateShowConsoleAction()
822
823        for i, a in enumerate(a for a in self.actionGroupTaskView.actions()
824                              if a.isVisible()):
825            a.setShortcut('Alt+%d' % (i + 1))
826
827    @pyqtSlot()
828    def _updateTaskTabVisibilityAction(self):
829        rw = self._currentRepoWidget()
830        self.actionTaskTabVisible.setChecked(bool(rw) and rw.isTaskTabVisible())
831
832    @pyqtSlot()
833    def _updateHistoryActions(self):
834        'Update back / forward actions'
835        rw = self._currentRepoWidget()
836        self.actionBack.setEnabled(bool(rw and rw.canGoBack()))
837        self.actionForward.setEnabled(bool(rw and rw.canGoForward()))
838
839    @pyqtSlot()
840    def repoTabChanged(self):
841        self._updateHistoryActions()
842        self._updateMenu()
843        self._updateWindowTitle()
844
845    @pyqtSlot(str)
846    def _onCurrentRepoChanged(self, curpath):
847        curpath = pycompat.unicode(curpath)
848        self._console.setCurrentRepoRoot(curpath or None)
849        self.reporegistry.setActiveTabRepo(curpath)
850        if curpath:
851            repoagent = self._repomanager.repoAgent(curpath)
852            repo = repoagent.rawRepo()
853            self.mqpatches.setRepoAgent(repoagent)
854            self._setupCustomTools(repo.ui)
855            self._setupUrlCombo(repoagent)
856            self._updateAbortAction(repoagent)
857        else:
858            self.mqpatches.setRepoAgent(None)
859            self.actionAbort.setEnabled(False)
860
861    @pyqtSlot()
862    def _setHistoryColumns(self):
863        """Display the column selection dialog"""
864        w = self._currentRepoWidget()
865        assert w
866        w.repoview.setHistoryColumns()
867
868    def _repotogglefwd(self, name):
869        """Return function to forward action to the current repo tab"""
870        def forwarder(checked):
871            w = self._currentRepoWidget()
872            if w:
873                getattr(w, name)(checked)
874        return forwarder
875
876    def _repofwd(self, name, params=None, namedparams=None):
877        """Return function to forward action to the current repo tab"""
878        if params is None:
879            params = []
880        if namedparams is None:
881            namedparams = {}
882
883        def forwarder():
884            w = self._currentRepoWidget()
885            if w:
886                getattr(w, name)(*params, **namedparams)
887
888        return forwarder
889
890    @pyqtSlot()
891    def refresh(self):
892        clear = QApplication.keyboardModifiers() & Qt.ControlModifier
893        w = self._currentRepoWidget()
894        if w:
895            # check unnoticed changes to emit corresponding signals
896            repoagent = self._repomanager.repoAgent(w.repoRootPath())
897            if clear:
898                repoagent.clearStatus()
899            repoagent.pollStatus()
900            # TODO if all objects are responsive to repository signals, some
901            # of the following actions are not necessary
902            w.reload()
903
904    @pyqtSlot(QAction)
905    def _runSyncAction(self, action):
906        w = self._currentRepoWidget()
907        if w:
908            op = str(action.data())
909            w.setSyncUrl(self._syncUrlFor(op) or '')
910            getattr(w, op)()
911
912    @pyqtSlot(QAction)
913    def _runSyncAllTabsAction(self, action):
914        originalIndex = self.repoTabsWidget.currentIndex()
915        for index in range(0, self.repoTabsWidget.count()):
916            self.repoTabsWidget.setCurrentIndex(index)
917            self._runSyncAction(action)
918        self.repoTabsWidget.setCurrentIndex(originalIndex)
919
920    @pyqtSlot()
921    def _runSyncBookmarks(self):
922        w = self._currentRepoWidget()
923        if w:
924            # the sync bookmark dialog is bidirectional but is only able to
925            # handle one remote location therefore we use the push location
926            w.setSyncUrl(self._syncUrlFor('push') or '')
927            w.syncBookmark()
928
929    @pyqtSlot()
930    def _abortCommands(self):
931        root = self.currentRepoRootPath()
932        if not root:
933            return
934        repoagent = self._repomanager.repoAgent(root)
935        repoagent.abortCommands()
936
937    def _updateAbortAction(self, repoagent):
938        self.actionAbort.setEnabled(repoagent.isBusy())
939
940    @pyqtSlot(str)
941    def _onBusyChanged(self, root):
942        repoagent = self._repomanager.repoAgent(root)
943        self._updateAbortAction(repoagent)
944        if not repoagent.isBusy():
945            self.statusbar.clearRepoProgress(root)
946        self.statusbar.setRepoBusy(root, repoagent.isBusy())
947
948    def serve(self):
949        self._dialogs.open(Workbench._createServeDialog)
950
951    def _createServeDialog(self):
952        w = self._currentRepoWidget()
953        if w:
954            return serve.run(w.repo.ui, root=w.repo.root)
955        else:
956            return serve.run(self.ui)
957
958    def loadall(self):
959        w = self._currentRepoWidget()
960        if w:
961            w.repoview.model().loadall()
962
963    def _gotorev(self):
964        rev, ok = qtlib.getTextInput(self,
965                                     _("Goto revision"),
966                                     _("Enter revision identifier"))
967        if ok:
968            w = self._currentRepoWidget()
969            assert w
970            w.gotoRev(rev)
971
972    @pyqtSlot(str)
973    def gotorev(self, rev):
974        w = self._currentRepoWidget()
975        if w:
976            w.repoview.goto(rev)
977
978    def newWorkbench(self):
979        cmdline = list(paths.get_thg_command())
980        cmdline.extend(['workbench', '--nofork', '--newworkbench'])
981        subprocess.Popen(cmdline, creationflags=qtlib.openflags)
982
983    def newRepository(self):
984        """ Run init dialog """
985        from tortoisehg.hgqt.hginit import InitDialog
986        path = self.currentRepoRootPath() or '.'
987        dlg = InitDialog(self.ui, path, self)
988        if dlg.exec_() == 0:
989            self.openRepo(dlg.destination(), False)
990
991    @pyqtSlot()
992    @pyqtSlot(str)
993    def cloneRepository(self, uroot=None):
994        """ Run clone dialog """
995        # it might be better to reuse existing CloneDialog
996        dlg = self._dialogs.openNew(Workbench._createCloneDialog)
997        if not uroot:
998            uroot = self.currentRepoRootPath()
999        if uroot:
1000            dlg.setSource(uroot)
1001            dlg.setDestination(uroot + '-clone')
1002
1003    def _createCloneDialog(self):
1004        from tortoisehg.hgqt.clone import CloneDialog
1005        dlg = CloneDialog(self.ui, self._config, parent=self)
1006        dlg.clonedRepository.connect(self._openClonedRepo)
1007        return dlg
1008
1009    @pyqtSlot(str, str)
1010    def _openClonedRepo(self, root, sourceroot):
1011        root = pycompat.unicode(root)
1012        sourceroot = pycompat.unicode(sourceroot)
1013        self.reporegistry.addClonedRepo(root, sourceroot)
1014        self.showRepo(root)
1015
1016    def openRepository(self):
1017        """ Open repo from File menu """
1018        caption = _('Select repository directory to open')
1019        root = self.currentRepoRootPath()
1020        if root:
1021            cwd = os.path.dirname(root)
1022        else:
1023            cwd = hglib.getcwdu()
1024        FD = QFileDialog
1025        path = FD.getExistingDirectory(self, caption, cwd,
1026                                       FD.ShowDirsOnly | FD.ReadOnly)
1027        self.openRepo(path, False)
1028
1029    def _currentRepoWidget(self):
1030        return self.repoTabsWidget.currentWidget()
1031
1032    def currentRepoRootPath(self):
1033        return self.repoTabsWidget.currentRepoRootPath()
1034
1035    def onAbout(self, *args):
1036        """ Display about dialog """
1037        from tortoisehg.hgqt.about import AboutDialog
1038        ad = AboutDialog(self)
1039        ad.finished.connect(ad.deleteLater)
1040        ad.exec_()
1041
1042    def onHelp(self, *args):
1043        """ Display online help """
1044        qtlib.openhelpcontents('workbench.html')
1045
1046    def onHelpExplorer(self, *args):
1047        """ Display online help for shell extension """
1048        qtlib.openhelpcontents('explorer.html')
1049
1050    def onReadme(self, *args):
1051        """Display the README file or URL for the current repo, or the global
1052        README if no repo is open"""
1053        readme = None
1054        def getCurrentReadme(repo):
1055            """
1056            Get the README file that is configured for the current repo.
1057
1058            README files can be set in 3 ways, which are checked in the
1059            following order of decreasing priority:
1060            - From the tortoisehg.readme key on the current repo's configuration
1061              file
1062            - An existing "README" file found on the repository root
1063                * Valid README files are those called README and whose extension
1064                  is one of the following:
1065                    ['', '.txt', '.html', '.pdf', '.doc', '.docx', '.ppt', '.pptx',
1066                     '.markdown', '.textile', '.rdoc', '.org', '.creole',
1067                     '.mediawiki','.rst', '.asciidoc', '.pod']
1068                * Note that the match is CASE INSENSITIVE on ALL OSs.
1069            - From the tortoisehg.readme key on the user's global configuration file
1070            """
1071            readme = None
1072            if repo:
1073                # Try to get the README configured for the repo of the current tab
1074                readmeglobal = self.ui.config(b'tortoisehg', b'readme')
1075                if readmeglobal:
1076                    # Note that repo.ui.config() falls back to the self.ui.config()
1077                    # if the key is not set on the current repo's configuration file
1078                    readme = repo.ui.config(b'tortoisehg', b'readme')
1079                    if readmeglobal != readme:
1080                        # The readme is set on the current repo configuration file
1081                        return readme
1082
1083                # Otherwise try to see if there is a file at the root of the
1084                # repository that matches any of the valid README file names
1085                # (in a non case-sensitive way)
1086                # Note that we try to match the valid README names in order
1087                validreadmes = [b'readme.txt', b'read.me', b'readme.html',
1088                                b'readme.pdf', b'readme.doc', b'readme.docx',
1089                                b'readme.ppt', b'readme.pptx',
1090                                b'readme.md', b'readme.markdown', b'readme.mkdn',
1091                                b'readme.rst', b'readme.textile', b'readme.rdoc',
1092                                b'readme.asciidoc', b'readme.org', b'readme.creole',
1093                                b'readme.mediawiki', b'readme.pod', b'readme']
1094
1095                readmefiles = [filename for filename in os.listdir(repo.root)
1096                               if filename.lower().startswith(b'read')]
1097                for validname in validreadmes:
1098                    for filename in readmefiles:
1099                        if filename.lower() == validname:
1100                            return repo.wjoin(filename)
1101
1102            # Otherwise try use the global setting (or None if readme is just
1103            # not configured)
1104            return readmeglobal
1105
1106        w = self._currentRepoWidget()
1107        if w:
1108            # Try to get the help doc from the current repo tap
1109            readme = getCurrentReadme(w.repo)
1110
1111        if readme:
1112            qtlib.openlocalurl(os.path.expanduser(os.path.expandvars(readme)))
1113        else:
1114            qtlib.WarningMsgBox(_("README not configured"),
1115                _("A README file is not configured for the current repository.<p>"
1116                "To configure a README file for a repository, "
1117                "open the repository settings file, add a '<i>readme</i>' "
1118                "key to the '<i>tortoisehg</i>' section, and set it "
1119                "to the filename or URL of your repository's README file."))
1120
1121    def _storeSettings(self, repostosave, lastactiverepo):
1122        s = QSettings()
1123        wb = "Workbench/"
1124        s.setValue(wb + 'geometry', self.saveGeometry())
1125        s.setValue(wb + 'windowState', self.saveState())
1126        s.setValue(wb + 'dockedConsole', self._actionDockedConsole.isChecked())
1127        s.setValue(wb + 'saveRepos', self.actionSaveRepos.isChecked())
1128        s.setValue(wb + 'saveLastSyncPaths',
1129            self.actionSaveLastSyncPaths.isChecked())
1130        s.setValue(wb + 'lastactiverepo', lastactiverepo)
1131        s.setValue(wb + 'openrepos', ','.join(repostosave))
1132        s.beginWriteArray('lastreposyncpaths')
1133        lastreposyncpaths = {}
1134        if self.actionSaveLastSyncPaths.isChecked():
1135            lastreposyncpaths = self._lastRepoSyncPath
1136        for n, root in enumerate(sorted(lastreposyncpaths)):
1137            s.setArrayIndex(n)
1138            s.setValue('root', root)
1139            s.setValue('path', self._lastRepoSyncPath[root])
1140        s.endArray()
1141
1142    def restoreSettings(self):
1143        s = QSettings()
1144        wb = "Workbench/"
1145        self.restoreGeometry(qtlib.readByteArray(s, wb + 'geometry'))
1146        self.restoreState(qtlib.readByteArray(s, wb + 'windowState'))
1147        self._actionDockedConsole.setChecked(
1148            qtlib.readBool(s, wb + 'dockedConsole', True))
1149
1150        lastreposyncpaths = {}
1151        npaths = s.beginReadArray('lastreposyncpaths')
1152        for n in range(npaths):
1153            s.setArrayIndex(n)
1154            root = qtlib.readString(s, 'root')
1155            lastreposyncpaths[root] = qtlib.readString(s, 'path')
1156        s.endArray()
1157        self._lastRepoSyncPath = lastreposyncpaths
1158
1159        save = qtlib.readBool(s, wb + 'saveRepos')
1160        self.actionSaveRepos.setChecked(save)
1161        savelastsyncpaths = qtlib.readBool(s, wb + 'saveLastSyncPaths')
1162        self.actionSaveLastSyncPaths.setChecked(savelastsyncpaths)
1163
1164        openreposvalue = qtlib.readString(s, wb + 'openrepos')
1165        if openreposvalue:
1166            openrepos = openreposvalue.split(',')
1167        else:
1168            openrepos = []
1169        # Note that if a "root" has been passed to the "thg" command,
1170        # "lastactiverepo" will have no effect
1171        lastactiverepo = qtlib.readString(s, wb + 'lastactiverepo')
1172        self.repoTabsWidget.restoreRepos(openrepos, lastactiverepo)
1173
1174        # Clear the lastactiverepo and the openrepos list once the workbench state
1175        # has been reload, so that opening additional workbench windows does not
1176        # reopen these repos again
1177        s.setValue(wb + 'openrepos', '')
1178        s.setValue(wb + 'lastactiverepo', '')
1179
1180    def goto(self, root, rev):
1181        if self.repoTabsWidget.selectRepo(hglib.tounicode(root)):
1182            rw = self.repoTabsWidget.currentWidget()
1183            rw.goto(rev)
1184
1185    def closeEvent(self, event):
1186        repostosave = []
1187        lastactiverepo = ''
1188        if self.actionSaveRepos.isChecked():
1189            tw = self.repoTabsWidget
1190            repostosave = pycompat.maplist(tw.repoRootPath,
1191                                           pycompat.xrange(tw.count()))
1192            lastactiverepo = tw.currentRepoRootPath()
1193        if not self.repoTabsWidget.closeAllTabs():
1194            event.ignore()
1195        else:
1196            self._storeSettings(repostosave, lastactiverepo)
1197            self.reporegistry.close()
1198
1199    @pyqtSlot()
1200    def closeCurrentRepoTab(self):
1201        """close the current repo tab"""
1202        self.repoTabsWidget.closeTab(self.repoTabsWidget.currentIndex())
1203
1204    def explore(self):
1205        root = self.currentRepoRootPath()
1206        if root:
1207            qtlib.openlocalurl(root)
1208
1209    def terminal(self):
1210        w = self._currentRepoWidget()
1211        if w:
1212            qtlib.openshell(w.repo.root, hglib.fromunicode(w.repoDisplayName()),
1213                            w.repo.ui)
1214
1215    @pyqtSlot()
1216    def editSettings(self, focus=None):
1217        sd = SettingsDialog(configrepo=False, focus=focus,
1218                            parent=self,
1219                            root=hglib.fromunicode(self.currentRepoRootPath()))
1220        sd.exec_()
1221
1222    @pyqtSlot()
1223    def _editCustomToolsSettings(self):
1224        self.editSettings('tools')
1225
1226    @pyqtSlot()
1227    def _openShortcutSettingsDialog(self):
1228        dlg = shortcutsettings.ShortcutSettingsDialog(
1229            self._actionregistry, self)
1230        dlg.exec_()
1231