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