/tortoisehg/hgqt/settings.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 1015 lines · 879 code · 109 blank · 27 comment · 145 complexity · 1170bcfa172b9acf5422de7a38cc8aa8 MD5 · raw file

  1. # settings.py - Configuration dialog for TortoiseHg and Mercurial
  2. #
  3. # Copyright 2010 Steve Borho <steve@borho.org>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. import os
  8. from mercurial import ui, util, error
  9. from tortoisehg.util import hglib, settings, paths, wconfig, i18n
  10. from tortoisehg.hgqt.i18n import _
  11. from tortoisehg.hgqt import qtlib, qscilib, thgrepo
  12. from PyQt4.QtCore import *
  13. from PyQt4.QtGui import *
  14. # Technical Debt
  15. # stacked widget or pages need to be scrollable
  16. # we need a consistent icon set
  17. # connect to thgrepo.configChanged signal and refresh
  18. _unspecstr = _('<unspecified>')
  19. ENTRY_WIDTH = 300
  20. class SettingsCombo(QComboBox):
  21. def __init__(self, parent=None, **opts):
  22. QComboBox.__init__(self, parent, toolTip=opts['tooltip'])
  23. self.opts = opts
  24. self.setEditable(opts.get('canedit', False))
  25. self.setValidator(opts.get('validator', None))
  26. self.defaults = opts.get('defaults', [])
  27. if self.defaults and self.isEditable():
  28. self.setCompleter(QCompleter(self.defaults, self))
  29. self.curvalue = None
  30. self.loaded = False
  31. if 'nohist' in opts:
  32. self.previous = []
  33. else:
  34. settings = opts['settings']
  35. slist = settings.value('settings/'+opts['cpath']).toStringList()
  36. self.previous = [s for s in slist if s]
  37. self.setFixedWidth(ENTRY_WIDTH)
  38. def resetList(self):
  39. self.clear()
  40. ucur = hglib.tounicode(self.curvalue)
  41. if self.opts.get('defer') and not self.loaded:
  42. if self.curvalue == None: # unspecified
  43. self.addItem(_unspecstr)
  44. else:
  45. self.addItem(ucur or '...')
  46. return
  47. self.addItem(_unspecstr)
  48. curindex = None
  49. for s in self.defaults:
  50. if ucur == s:
  51. curindex = self.count()
  52. self.addItem(s)
  53. if self.defaults and self.previous:
  54. self.insertSeparator(len(self.defaults)+1)
  55. for m in self.previous:
  56. if ucur == m and not curindex:
  57. curindex = self.count()
  58. self.addItem(m)
  59. if curindex is not None:
  60. self.setCurrentIndex(curindex)
  61. elif self.curvalue is None:
  62. self.setCurrentIndex(0)
  63. elif self.curvalue:
  64. self.addItem(ucur)
  65. self.setCurrentIndex(self.count()-1)
  66. else: # empty string
  67. self.setEditText(ucur)
  68. def showPopup(self):
  69. if self.opts.get('defer') and not self.loaded:
  70. self.defaults = self.opts['defer']()
  71. self.loaded = True
  72. self.resetList()
  73. QComboBox.showPopup(self)
  74. ## common APIs for all edit widgets
  75. def setValue(self, curvalue):
  76. self.curvalue = curvalue
  77. self.resetList()
  78. def value(self):
  79. utext = self.currentText()
  80. if utext == _unspecstr:
  81. return None
  82. if 'nohist' in self.opts or utext in self.defaults + self.previous or not utext:
  83. return hglib.fromunicode(utext)
  84. self.previous.insert(0, utext)
  85. self.previous = self.previous[:10]
  86. settings = QSettings()
  87. settings.setValue('settings/'+self.opts['cpath'], self.previous)
  88. return hglib.fromunicode(utext)
  89. def isDirty(self):
  90. return self.value() != self.curvalue
  91. class PasswordEntry(QLineEdit):
  92. def __init__(self, parent=None, **opts):
  93. QLineEdit.__init__(self, parent, toolTip=opts['tooltip'])
  94. self.opts = opts
  95. self.curvalue = None
  96. self.setEchoMode(QLineEdit.Password)
  97. self.setFixedWidth(ENTRY_WIDTH)
  98. ## common APIs for all edit widgets
  99. def setValue(self, curvalue):
  100. self.curvalue = curvalue
  101. if curvalue:
  102. self.setText(hglib.tounicode(curvalue))
  103. else:
  104. self.setText('')
  105. def value(self):
  106. utext = self.text()
  107. return utext and hglib.fromunicode(utext) or None
  108. def isDirty(self):
  109. return self.value() != self.curvalue
  110. class FontEntry(QPushButton):
  111. def __init__(self, parent=None, **opts):
  112. QPushButton.__init__(self, parent, toolTip=opts['tooltip'])
  113. self.opts = opts
  114. self.curvalue = None
  115. self.clicked.connect(self.on_clicked)
  116. cpath = self.opts['cpath']
  117. assert cpath.startswith('tortoisehg.')
  118. self.fname = cpath[11:]
  119. self.setFixedWidth(ENTRY_WIDTH)
  120. def on_clicked(self, checked):
  121. def newFont(font):
  122. self.setText(font.toString())
  123. thgf.setFont(font)
  124. thgf = qtlib.getfont(self.fname)
  125. origfont = self.currentFont() or thgf.font()
  126. dlg = QFontDialog(self)
  127. dlg.currentFontChanged.connect(newFont)
  128. font, isok = dlg.getFont(origfont, self)
  129. self.setText(font.toString())
  130. thgf.setFont(font)
  131. def currentFont(self):
  132. """currently selected QFont if specified"""
  133. if not self.value():
  134. return None
  135. f = QFont()
  136. f.fromString(self.value())
  137. return f
  138. ## common APIs for all edit widgets
  139. def setValue(self, curvalue):
  140. self.curvalue = curvalue
  141. if curvalue:
  142. self.setText(hglib.tounicode(curvalue))
  143. else:
  144. self.setText(_unspecstr)
  145. def value(self):
  146. utext = self.text()
  147. if utext == _unspecstr:
  148. return None
  149. else:
  150. return hglib.fromunicode(utext)
  151. def isDirty(self):
  152. return self.value() != self.curvalue
  153. class SettingsCheckBox(QCheckBox):
  154. def __init__(self, parent=None, **opts):
  155. QCheckBox.__init__(self, parent, toolTip=opts['tooltip'])
  156. self.opts = opts
  157. self.curvalue = None
  158. self.setText(opts['label'])
  159. self.valfunc = self.opts['valfunc']
  160. self.toggled.connect(self.valfunc)
  161. def setValue(self, curvalue):
  162. if self.curvalue == None:
  163. self.curvalue = curvalue
  164. self.setChecked(curvalue)
  165. def value(self):
  166. return self.isChecked()
  167. def isDirty(self):
  168. return self.value() != self.curvalue
  169. def genEditCombo(opts, defaults=[]):
  170. opts['canedit'] = True
  171. opts['defaults'] = defaults
  172. return SettingsCombo(**opts)
  173. def genIntEditCombo(opts):
  174. 'EditCombo, only allows integer values'
  175. opts['canedit'] = True
  176. opts['validator'] = QIntValidator()
  177. return SettingsCombo(**opts)
  178. def genPasswordEntry(opts):
  179. 'Generate a password entry box'
  180. return PasswordEntry(**opts)
  181. def genDefaultCombo(opts, defaults=[]):
  182. 'user must select from a list'
  183. opts['defaults'] = defaults
  184. opts['nohist'] = True
  185. return SettingsCombo(**opts)
  186. def genBoolCombo(opts):
  187. 'true, false, unspecified'
  188. opts['defaults'] = ['True', 'False']
  189. opts['nohist'] = True
  190. return SettingsCombo(**opts)
  191. def genDeferredCombo(opts, func):
  192. 'Values retrieved from a function at popup time'
  193. opts['defer'] = func
  194. opts['nohist'] = True
  195. return SettingsCombo(**opts)
  196. def genFontEdit(opts):
  197. return FontEntry(**opts)
  198. def findDiffTools():
  199. return hglib.difftools(ui.ui())
  200. def findMergeTools():
  201. return hglib.mergetools(ui.ui())
  202. def genCheckBox(opts):
  203. opts['nohist'] = True
  204. return SettingsCheckBox(**opts)
  205. class _fi(object):
  206. """Information of each field"""
  207. __slots__ = ('label', 'cpath', 'values', 'tooltip',
  208. 'restartneeded', 'globalonly')
  209. def __init__(self, label, cpath, values, tooltip,
  210. restartneeded=False, globalonly=False):
  211. self.label = label
  212. self.cpath = cpath
  213. self.values = values
  214. self.tooltip = tooltip
  215. self.restartneeded = restartneeded
  216. self.globalonly = globalonly
  217. INFO = (
  218. ({'name': 'general', 'label': 'TortoiseHg', 'icon': 'thg_logo'}, (
  219. _fi(_('UI Language'), 'tortoisehg.ui.language',
  220. (genDeferredCombo, i18n.availablelanguages),
  221. _('Specify your preferred user interface language (restart needed)'),
  222. restartneeded=True, globalonly=True),
  223. _fi(_('Three-way Merge Tool'), 'ui.merge',
  224. (genDeferredCombo, findMergeTools),
  225. _('Graphical merge program for resolving merge conflicts. If left'
  226. ' unspecified, Mercurial will use the first applicable tool it finds'
  227. ' on your system or use its internal merge tool that leaves conflict'
  228. ' markers in place. Chose internal:merge to force conflict markers,'
  229. ' internal:prompt to always select local or other, or internal:dump'
  230. ' to leave files in the working directory for manual merging')),
  231. _fi(_('Visual Diff Tool'), 'tortoisehg.vdiff',
  232. (genDeferredCombo, findDiffTools),
  233. _('Specify visual diff tool, as described in the [merge-tools]'
  234. ' section of your Mercurial configuration files. If left'
  235. ' unspecified, TortoiseHg will use the selected merge tool.'
  236. ' Failing that it uses the first applicable tool it finds.')),
  237. _fi(_('Visual Editor'), 'tortoisehg.editor', genEditCombo,
  238. _('Specify the visual editor used to view files. Format:<br>'
  239. 'myeditor -flags [$FILE --num=$LINENUM][--search $SEARCH]<br><br>'
  240. 'See <a href="%s">OpenAtLine</a>'
  241. % 'http://bitbucket.org/tortoisehg/thg/wiki/OpenAtLine')),
  242. _fi(_('Shell'), 'tortoisehg.shell', genEditCombo,
  243. _('Specify your preferred terminal shell application')),
  244. _fi(_('Immediate Operations'), 'tortoisehg.immediate', genEditCombo,
  245. _('Space separated list of shell operations you would like '
  246. 'to be performed immediately, without user interaction. '
  247. 'Commands are "add remove revert forget". '
  248. 'Default: None (leave blank)')),
  249. _fi(_('Poll Frequency'), 'tortoisehg.pollfreq', genIntEditCombo,
  250. _('The period (in milliseconds) between modification time polling of '
  251. 'key repository files, looking for changes. Values under '
  252. '100ms are ignored. Default: 500')),
  253. _fi(_('Tab Width'), 'tortoisehg.tabwidth', genIntEditCombo,
  254. _('Specify the number of spaces that tabs expand to in various'
  255. ' TortoiseHg windows.'
  256. ' Default: 0, Not expanded')),
  257. _fi(_('Max Diff Size'), 'tortoisehg.maxdiff', genIntEditCombo,
  258. _('The maximum size file (in KB) that TortoiseHg will '
  259. 'show changes for in the changelog, status, and commit windows.'
  260. ' A value of zero implies no limit. Default: 1024 (1MB)')),
  261. _fi(_('Capture stderr'), 'tortoisehg.stderrcapt', genBoolCombo,
  262. _('Redirect stderr to a buffer which is parsed at the end of'
  263. ' the process for runtime errors. Default: True')),
  264. _fi(_('Fork GUI'), 'tortoisehg.guifork', genBoolCombo,
  265. _('When running from the command line, fork a background'
  266. ' process to run graphical dialogs. Default: True')),
  267. _fi(_('Full Path Title'), 'tortoisehg.fullpath', genBoolCombo,
  268. _('Show a full directory path of the repository in the dialog title'
  269. ' instead of just the root directory name. Default: False')),
  270. _fi(_('Auto-resolve merges'), 'tortoisehg.autoresolve', genBoolCombo,
  271. _('Indicates whether TortoiseHg should attempt to automatically resolve'
  272. ' changes from both sides to the same file, and only report merge'
  273. ' conflicts when this is not possible. When False, all files with'
  274. ' changes on both sides of the merge will report as conflicting, even'
  275. ' if the edits are to different parts of the file. In either case,'
  276. ' when conflicts occur, the user will be invited to review and'
  277. ' resolve changes manually. Default: False.')),
  278. )),
  279. ({'name': 'log', 'label': _('Workbench'), 'icon': 'menulog'}, (
  280. _fi(_('Author Coloring'), 'tortoisehg.authorcolor', genBoolCombo,
  281. _('Color changesets by author name. If not enabled,'
  282. ' the changes are colored green for merge, red for'
  283. ' non-trivial parents, black for normal.'
  284. ' Default: False')),
  285. _fi(_('Task Tabs'), 'tortoisehg.tasktabs', (genDefaultCombo,
  286. ['east', 'west', 'off']),
  287. _('Show tabs along the side of the bottom half of each repo'
  288. ' widget allowing one to switch task tabs without using the toolbar.'
  289. ' Default: off')),
  290. _fi(_('Long Summary'), 'tortoisehg.longsummary', genBoolCombo,
  291. _('If true, concatenate multiple lines of changeset summary'
  292. ' until they reach 80 characters.'
  293. ' Default: False')),
  294. _fi(_('Log Batch Size'), 'tortoisehg.graphlimit', genIntEditCombo,
  295. _('The number of revisions to read and display in the'
  296. ' changelog viewer in a single batch.'
  297. ' Default: 500')),
  298. _fi(_('Dead Branches'), 'tortoisehg.deadbranch', genEditCombo,
  299. _('Comma separated list of branch names that should be ignored'
  300. ' when building a list of branch names for a repository.'
  301. ' Default: None (leave blank)')),
  302. _fi(_('Branch Colors'), 'tortoisehg.branchcolors', genEditCombo,
  303. _('Space separated list of branch names and colors of the form'
  304. ' branch:#XXXXXX. Spaces and colons in the branch name must be'
  305. ' escaped using a backslash (\\). Likewise some other characters'
  306. ' can be escaped in this way, e.g. \\u0040 will be decoded to the'
  307. ' @ character, and \\n to a linefeed.'
  308. ' Default: None (leave blank)')),
  309. _fi(_('Hide Tags'), 'tortoisehg.hidetags', genEditCombo,
  310. _('Space separated list of tags that will not be shown.'
  311. ' Useful example: Specify "qbase qparent qtip" to hide the'
  312. ' standard tags inserted by the Mercurial Queues Extension.'
  313. ' Default: None (leave blank)')),
  314. _fi(_('After Pull Operation'), 'tortoisehg.postpull', (genDefaultCombo,
  315. ['none', 'update', 'fetch', 'rebase']),
  316. _('Operation which is performed directly after a successful pull.'
  317. ' update equates to pull --update, fetch equates to the fetch'
  318. ' extension, rebase equates to pull --rebase. Default: none')),
  319. )),
  320. ({'name': 'commit', 'label': _('Commit'), 'icon': 'menucommit'}, (
  321. _fi(_('Username'), 'ui.username', genEditCombo,
  322. _('Name associated with commits. The common format is<br>'
  323. '"Full Name &lt;email@example.com&gt;"')),
  324. _fi(_('Summary Line Length'), 'tortoisehg.summarylen', genIntEditCombo,
  325. _('Suggested length of commit message lines. A red vertical'
  326. ' line will mark this length. CTRL-E will reflow the current'
  327. ' paragraph to the specified line length. Default: 80')),
  328. _fi(_('Close After Commit'), 'tortoisehg.closeci', genBoolCombo,
  329. _('Close the commit tool after every successful'
  330. ' commit. Default: False')),
  331. _fi(_('Push After Commit'), 'tortoisehg.cipushafter', genEditCombo,
  332. _('Attempt to push to specified URL or alias after each successful'
  333. ' commit. Default: No push')),
  334. _fi(_('Auto Commit List'), 'tortoisehg.autoinc', genEditCombo,
  335. _('Comma separated list of files that are automatically included'
  336. ' in every commit. Intended for use only as a repository setting.'
  337. ' Default: None (leave blank)')),
  338. _fi(_('Auto Exclude List'), 'tortoisehg.ciexclude', genEditCombo,
  339. _('Comma separated list of files that are automatically unchecked'
  340. ' when the status, and commit dialogs are opened.'
  341. ' Default: None (leave blank)')),
  342. _fi(_('English Messages'), 'tortoisehg.engmsg', genBoolCombo,
  343. _('Generate English commit messages even if LANGUAGE or LANG'
  344. ' environment variables are set to a non-English language.'
  345. ' This setting is used by the Merge, Tag and Backout dialogs.'
  346. ' Default: False')),
  347. )),
  348. ({'name': 'web', 'label': _('Web Server'), 'icon': 'proxy'}, (
  349. _fi(_('Name'), 'web.name', genEditCombo,
  350. _('Repository name to use in the web interface, and by TortoiseHg '
  351. ' as a shorthand name. Default is the working directory.')),
  352. _fi(_('Description'), 'web.description', genEditCombo,
  353. _("Textual description of the repository's purpose or"
  354. ' contents.')),
  355. _fi(_('Contact'), 'web.contact', genEditCombo,
  356. _('Name or email address of the person in charge of the'
  357. ' repository.')),
  358. _fi(_('Style'), 'web.style', (genDefaultCombo,
  359. ['paper', 'monoblue', 'coal', 'spartan', 'gitweb', 'old']),
  360. _('Which template map style to use')),
  361. _fi(_('Archive Formats'), 'web.allow_archive',
  362. (genEditCombo, ['bz2', 'gz', 'zip']),
  363. _('Comma separated list of archive formats allowed for'
  364. ' downloading')),
  365. _fi(_('Port'), 'web.port', genIntEditCombo, _('Port to listen on')),
  366. _fi(_('Push Requires SSL'), 'web.push_ssl', genBoolCombo,
  367. _('Whether to require that inbound pushes be transported'
  368. ' over SSL to prevent password sniffing.')),
  369. _fi(_('Stripes'), 'web.stripes', genIntEditCombo,
  370. _('How many lines a "zebra stripe" should span in multiline output.'
  371. ' Default is 1; set to 0 to disable.')),
  372. _fi(_('Max Files'), 'web.maxfiles', genIntEditCombo,
  373. _('Maximum number of files to list per changeset. Default: 10')),
  374. _fi(_('Max Changes'), 'web.maxchanges', genIntEditCombo,
  375. _('Maximum number of changes to list on the changelog. '
  376. 'Default: 10')),
  377. _fi(_('Allow Push'), 'web.allow_push', (genEditCombo, ['*']),
  378. _('Whether to allow pushing to the repository. If empty or not'
  379. ' set, push is not allowed. If the special value "*", any remote'
  380. ' user can push, including unauthenticated users. Otherwise, the'
  381. ' remote user must have been authenticated, and the authenticated'
  382. ' user name must be present in this list (separated by whitespace'
  383. ' or ","). The contents of the allow_push list are examined after'
  384. ' the deny_push list.')),
  385. _fi(_('Deny Push'), 'web.deny_push', (genEditCombo, ['*']),
  386. _('Whether to deny pushing to the repository. If empty or not set,'
  387. ' push is not denied. If the special value "*", all remote users'
  388. ' are denied push. Otherwise, unauthenticated users are all'
  389. ' denied, and any authenticated user name present in this list'
  390. ' (separated by whitespace or ",") is also denied. The contents'
  391. ' of the deny_push list are examined before the allow_push list.')),
  392. _fi(_('Encoding'), 'web.encoding', (genEditCombo, ['UTF-8']),
  393. _('Character encoding name')),
  394. )),
  395. ({'name': 'proxy', 'label': _('Proxy'), 'icon': QStyle.SP_DriveNetIcon}, (
  396. _fi(_('Host'), 'http_proxy.host', genEditCombo,
  397. _('Host name and (optional) port of proxy server, for'
  398. ' example "myproxy:8000"')),
  399. _fi(_('Bypass List'), 'http_proxy.no', genEditCombo,
  400. _('Optional. Comma-separated list of host names that'
  401. ' should bypass the proxy')),
  402. _fi(_('User'), 'http_proxy.user', genEditCombo,
  403. _('Optional. User name to authenticate with at the proxy server')),
  404. _fi(_('Password'), 'http_proxy.passwd', genPasswordEntry,
  405. _('Optional. Password to authenticate with at the proxy server')),
  406. )),
  407. ({'name': 'email', 'label': _('Email'), 'icon': QStyle.SP_ArrowForward}, (
  408. _fi(_('From'), 'email.from', genEditCombo,
  409. _('Email address to use in the "From" header and for'
  410. ' the SMTP envelope')),
  411. _fi(_('To'), 'email.to', genEditCombo,
  412. _('Comma-separated list of recipient email addresses')),
  413. _fi(_('Cc'), 'email.cc', genEditCombo,
  414. _('Comma-separated list of carbon copy recipient email addresses')),
  415. _fi(_('Bcc'), 'email.bcc', genEditCombo,
  416. _('Comma-separated list of blind carbon copy recipient'
  417. ' email addresses')),
  418. _fi(_('method'), 'email.method', (genEditCombo, ['smtp']),
  419. _('Optional. Method to use to send email messages. If value is'
  420. ' "smtp" (default), use SMTP (configured below). Otherwise, use as'
  421. ' name of program to run that acts like sendmail (takes "-f" option'
  422. ' for sender, list of recipients on command line, message on stdin).'
  423. ' Normally, setting this to "sendmail" or "/usr/sbin/sendmail"'
  424. ' is enough to use sendmail to send messages.')),
  425. _fi(_('SMTP Host'), 'smtp.host', genEditCombo,
  426. _('Host name of mail server')),
  427. _fi(_('SMTP Port'), 'smtp.port', genIntEditCombo,
  428. _('Port to connect to on mail server.'
  429. ' Default: 25')),
  430. _fi(_('SMTP TLS'), 'smtp.tls', genBoolCombo,
  431. _('Connect to mail server using TLS.'
  432. ' Default: False')),
  433. _fi(_('SMTP Username'), 'smtp.username', genEditCombo,
  434. _('Username to authenticate to mail server with')),
  435. _fi(_('SMTP Password'), 'smtp.password', genPasswordEntry,
  436. _('Password to authenticate to mail server with')),
  437. _fi(_('Local Hostname'), 'smtp.local_hostname', genEditCombo,
  438. _('Hostname the sender can use to identify itself to the'
  439. ' mail server.')),
  440. )),
  441. ({'name': 'diff', 'label': _('Diff'),
  442. 'icon': QStyle.SP_FileDialogContentsView}, (
  443. _fi(_('Patch EOL'), 'patch.eol', (genDefaultCombo,
  444. ['auto', 'strict', 'crlf', 'lf']),
  445. _('Normalize file line endings during and after patch to lf or'
  446. ' crlf. Strict does no normalization. Auto does per-file'
  447. ' detection, and is the recommended setting.'
  448. ' Default: strict')),
  449. _fi(_('Git Format'), 'diff.git', genBoolCombo,
  450. _('Use git extended diff header format.'
  451. ' Default: False')),
  452. _fi(_('MQ Git Format'), 'mq.git', (genDefaultCombo,
  453. ['auto', 'keep', 'yes', 'no']),
  454. _("If set to 'keep', mq will obey the [diff] section configuration while"
  455. " preserving existing git patches upon qrefresh. If set to 'yes' or"
  456. " 'no', mq will override the [diff] section and always generate git or"
  457. " regular patches, possibly losing data in the second case.")),
  458. _fi(_('No Dates'), 'diff.nodates', genBoolCombo,
  459. _('Do not include modification dates in diff headers.'
  460. ' Default: False')),
  461. _fi(_('Show Function'), 'diff.showfunc', genBoolCombo,
  462. _('Show which function each change is in.'
  463. ' Default: False')),
  464. _fi(_('Ignore White Space'), 'diff.ignorews', genBoolCombo,
  465. _('Ignore white space when comparing lines.'
  466. ' Default: False')),
  467. _fi(_('Ignore WS Amount'), 'diff.ignorewsamount', genBoolCombo,
  468. _('Ignore changes in the amount of white space.'
  469. ' Default: False')),
  470. _fi(_('Ignore Blank Lines'), 'diff.ignoreblanklines', genBoolCombo,
  471. _('Ignore changes whose lines are all blank.'
  472. ' Default: False')),
  473. )),
  474. ({'name': 'fonts', 'label': _('Fonts'), 'icon': 'fonts'}, (
  475. _fi(_('Message Font'), 'tortoisehg.fontcomment', genFontEdit,
  476. _('Font used to display commit messages. Default: monospace 10'),
  477. globalonly=True),
  478. _fi(_('Diff Font'), 'tortoisehg.fontdiff', genFontEdit,
  479. _('Font used to display text differences. Default: monospace 10'),
  480. globalonly=True),
  481. _fi(_('List Font'), 'tortoisehg.fontlist', genFontEdit,
  482. _('Font used to display file lists. Default: sans 9'),
  483. globalonly=True),
  484. _fi(_('ChangeLog Font'), 'tortoisehg.fontlog', genFontEdit,
  485. _('Font used to display changelog data. Default: monospace 10'),
  486. globalonly=True),
  487. _fi(_('Output Font'), 'tortoisehg.fontoutputlog', genFontEdit,
  488. _('Font used to display output messages. Default: sans 8'),
  489. globalonly=True),
  490. )),
  491. ({'name': 'extensions', 'label': _('Extensions'), 'icon': 'extensions'}, (
  492. )),
  493. ({'name': 'reviewboard', 'label': _('Review Board'), 'icon': 'reviewboard'}, (
  494. _fi(_('Server'), 'reviewboard.server', genEditCombo,
  495. _('Path to review board'
  496. ' example "http://demo.reviewboard.org"')),
  497. _fi(_('User'), 'reviewboard.user', genEditCombo,
  498. _('User name to authenticate with review board')),
  499. _fi(_('Password'), 'reviewboard.password', genPasswordEntry,
  500. _('Password to authenticate with review board')),
  501. _fi(_('Server Repository ID'), 'reviewboard.repoid', genEditCombo,
  502. _('The default repository id for this repo on the review board server')),
  503. _fi(_('Target Groups'), 'reviewboard.target_groups', genEditCombo,
  504. _('A comma separated list of target groups')),
  505. _fi(_('Target People'), 'reviewboard.target_people', genEditCombo,
  506. _('A comma separated list of target people')),
  507. )),
  508. )
  509. CONF_GLOBAL = 0
  510. CONF_REPO = 1
  511. class SettingsDialog(QDialog):
  512. 'Dialog for editing Mercurial.ini or hgrc'
  513. def __init__(self, configrepo=False, focus=None, parent=None, root=None):
  514. QDialog.__init__(self, parent)
  515. self.setWindowTitle(_('TortoiseHg Settings'))
  516. self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
  517. if not hasattr(wconfig.config(), 'write'):
  518. qtlib.ErrorMsgBox(_('Iniparse package not found'),
  519. _("Can't change settings without iniparse package - "
  520. 'view is readonly.'), parent=self)
  521. print 'Please install http://code.google.com/p/iniparse/'
  522. layout = QVBoxLayout()
  523. self.setLayout(layout)
  524. s = QSettings()
  525. self.settings = s
  526. self.restoreGeometry(s.value('settings/geom').toByteArray())
  527. def username():
  528. return util.username() or os.environ.get('USERNAME') or _('User')
  529. self.conftabs = QTabWidget()
  530. layout.addWidget(self.conftabs)
  531. utab = SettingsForm(rcpath=util.user_rcpath(), focus=focus)
  532. self.conftabs.addTab(utab, qtlib.geticon('settings_user'),
  533. _("%s's global settings") % username())
  534. utab.restartRequested.connect(self._pushRestartRequest)
  535. try:
  536. if root is None:
  537. root = paths.find_root()
  538. if root:
  539. repo = thgrepo.repository(ui.ui(), root)
  540. else:
  541. repo = None
  542. except error.RepoError:
  543. repo = None
  544. if configrepo:
  545. qtlib.ErrorMsgBox(_('No repository found'),
  546. _('no repo at ') + root, parent=self)
  547. if repo:
  548. reporcpath = os.sep.join([repo.root, '.hg', 'hgrc'])
  549. rtab = SettingsForm(rcpath=reporcpath, focus=focus)
  550. self.conftabs.addTab(rtab, qtlib.geticon('settings_repo'),
  551. _('%s repository settings') % repo.displayname)
  552. rtab.restartRequested.connect(self._pushRestartRequest)
  553. BB = QDialogButtonBox
  554. bb = QDialogButtonBox(BB.Ok|BB.Cancel)
  555. bb.accepted.connect(self.accept)
  556. bb.rejected.connect(self.reject)
  557. layout.addWidget(bb)
  558. self.bb = bb
  559. self._restartreqs = set()
  560. self.conftabs.setCurrentIndex(configrepo and CONF_REPO or CONF_GLOBAL)
  561. def isDirty(self):
  562. return util.any(self.conftabs.widget(i).isDirty()
  563. for i in xrange(self.conftabs.count()))
  564. @pyqtSlot(unicode)
  565. def _pushRestartRequest(self, key):
  566. self._restartreqs.add(unicode(key))
  567. def applyChanges(self):
  568. for i in xrange(self.conftabs.count()):
  569. self.conftabs.widget(i).applyChanges()
  570. if self._restartreqs:
  571. qtlib.InfoMsgBox(_('Settings'),
  572. _('Restart all TortoiseHg applications'
  573. ' for the following changes to take effect:'),
  574. ', '.join(sorted(self._restartreqs)))
  575. self._restartreqs.clear()
  576. def canExit(self):
  577. if self.isDirty():
  578. ret = qtlib.CustomPrompt(_('Confirm Exit'),
  579. _('Apply changes before exit?'), self,
  580. (_('&Yes'), _('&No (discard changes)'),
  581. _ ('Cancel')), default=2, esc=2).run()
  582. if ret == 2:
  583. return False
  584. elif ret == 0:
  585. self.applyChanges()
  586. return True
  587. return True
  588. def accept(self):
  589. self.applyChanges()
  590. s = self.settings
  591. s.setValue('settings/geom', self.saveGeometry())
  592. s.sync()
  593. QDialog.accept(self)
  594. def reject(self):
  595. if not self.canExit():
  596. return
  597. s = self.settings
  598. s.setValue('settings/geom', self.saveGeometry())
  599. s.sync()
  600. QDialog.reject(self)
  601. class SettingsForm(QWidget):
  602. """Widget for each settings file"""
  603. restartRequested = pyqtSignal(unicode)
  604. def __init__(self, rcpath, focus=None, parent=None):
  605. super(SettingsForm, self).__init__(parent)
  606. if isinstance(rcpath, (list, tuple)):
  607. self.rcpath = rcpath
  608. else:
  609. self.rcpath = [rcpath]
  610. layout = QVBoxLayout()
  611. self.setLayout(layout)
  612. tophbox = QHBoxLayout()
  613. layout.addLayout(tophbox)
  614. self.fnedit = QLineEdit()
  615. self.fnedit.setReadOnly(True)
  616. self.fnedit.setFrame(False)
  617. self.fnedit.setFocusPolicy(Qt.NoFocus)
  618. self.fnedit.setStyleSheet('QLineEdit { background: transparent; }')
  619. edit = QPushButton(_('Edit File'))
  620. edit.clicked.connect(self.editClicked)
  621. self.editbtn = edit
  622. reload = QPushButton(_('Reload'))
  623. reload.clicked.connect(self.reloadClicked)
  624. self.reloadbtn = reload
  625. tophbox.addWidget(QLabel(_('Settings File:')))
  626. tophbox.addWidget(self.fnedit)
  627. tophbox.addWidget(edit)
  628. tophbox.addWidget(reload)
  629. bothbox = QHBoxLayout()
  630. layout.addLayout(bothbox)
  631. pageList = QListWidget()
  632. pageList.setResizeMode(QListView.Fixed)
  633. stack = QStackedWidget()
  634. bothbox.addWidget(pageList, 0)
  635. bothbox.addWidget(stack, 1)
  636. pageList.currentRowChanged.connect(stack.setCurrentIndex)
  637. self.pages = {}
  638. self.stack = stack
  639. self.pageList = pageList
  640. desctext = QTextBrowser()
  641. desctext.setOpenExternalLinks(True)
  642. layout.addWidget(desctext)
  643. self.desctext = desctext
  644. self.settings = QSettings()
  645. # add page items to treeview
  646. for meta, info in INFO:
  647. if isinstance(meta['icon'], str):
  648. icon = qtlib.geticon(meta['icon'])
  649. else:
  650. style = QApplication.style()
  651. icon = QIcon()
  652. icon.addPixmap(style.standardPixmap(meta['icon']))
  653. item = QListWidgetItem(icon, meta['label'])
  654. pageList.addItem(item)
  655. self.addPage(meta['name'])
  656. self.refresh()
  657. self.focusField(focus or 'ui.merge')
  658. def editClicked(self):
  659. 'Open internal editor in stacked widget'
  660. if self.isDirty():
  661. ret = qtlib.CustomPrompt(_('Confirm Save'),
  662. _('Save changes before edit?'), self,
  663. (_('&Save'), _('&Discard'), _('Cancel')),
  664. default=2, esc=2).run()
  665. if ret == 0:
  666. self.applyChanges()
  667. elif ret == 2:
  668. return
  669. if qscilib.fileEditor(self.fn, foldable=True) == QDialog.Accepted:
  670. self.refresh()
  671. def refresh(self, *args):
  672. # refresh config values
  673. self.ini = self.loadIniFile(self.rcpath)
  674. self.readonly = not (hasattr(self.ini, 'write') and os.access(self.fn, os.W_OK))
  675. self.stack.setDisabled(self.readonly)
  676. self.fnedit.setText(hglib.tounicode(self.fn))
  677. for name, info, widgets in self.pages.values():
  678. if name == 'extensions':
  679. extsmentioned = False
  680. for row, w in enumerate(widgets):
  681. val = self.readCPath('extensions.' + w.opts['label'])
  682. if val == None:
  683. curvalue = False
  684. elif len(val) and val[0] == '!':
  685. curvalue = False
  686. extsmentioned = True
  687. else:
  688. curvalue = True
  689. extsmentioned = True
  690. w.setValue(curvalue)
  691. if not extsmentioned:
  692. # make sure widgets are shown properly,
  693. # even when no extensions mentioned in the config file
  694. self.validateextensions()
  695. else:
  696. for row, e in enumerate(info):
  697. curvalue = self.readCPath(e.cpath)
  698. widgets[row].setValue(curvalue)
  699. def isDirty(self):
  700. if self.readonly:
  701. return False
  702. for name, info, widgets in self.pages.values():
  703. for w in widgets:
  704. if w.isDirty():
  705. return True
  706. return False
  707. def reloadClicked(self):
  708. if self.isDirty():
  709. d = QMessageBox.question(self, _('Confirm Reload'),
  710. _('Unsaved changes will be lost.\n'
  711. 'Do you want to reload?'),
  712. QMessageBox.Ok | QMessageBox.Cancel)
  713. if d != QMessageBox.Ok:
  714. return
  715. self.refresh()
  716. def focusField(self, focusfield):
  717. 'Set page and focus to requested datum'
  718. for i, (meta, info) in enumerate(INFO):
  719. for n, e in enumerate(info):
  720. if e.cpath == focusfield:
  721. self.pageList.setCurrentRow(i)
  722. QTimer.singleShot(0, lambda:
  723. self.pages[meta['name']][2][n].setFocus())
  724. return
  725. def fillFrame(self, info):
  726. widgets = []
  727. frame = QFrame()
  728. form = QFormLayout()
  729. frame.setLayout(form)
  730. self.stack.addWidget(frame)
  731. for e in info:
  732. opts = {'label': e.label, 'cpath': e.cpath, 'tooltip': e.tooltip,
  733. 'settings':self.settings}
  734. if isinstance(e.values, tuple):
  735. func = e.values[0]
  736. w = func(opts, e.values[1])
  737. else:
  738. func = e.values
  739. w = func(opts)
  740. w.installEventFilter(self)
  741. if e.globalonly:
  742. w.setEnabled(self.rcpath == util.user_rcpath())
  743. lbl = QLabel(e.label)
  744. lbl.installEventFilter(self)
  745. lbl.setToolTip(e.tooltip)
  746. form.addRow(lbl, w)
  747. widgets.append(w)
  748. return widgets
  749. def fillExtensionsFrame(self):
  750. widgets = []
  751. frame = QFrame()
  752. grid = QGridLayout()
  753. frame.setLayout(grid)
  754. self.stack.addWidget(frame)
  755. allexts = hglib.allextensions()
  756. allextslist = list(allexts)
  757. MAXCOLUMNS = 3
  758. maxrows = (len(allextslist) + MAXCOLUMNS - 1) / MAXCOLUMNS
  759. i = 0
  760. extsinfo = ()
  761. for i, name in enumerate(sorted(allexts)):
  762. tt = hglib.tounicode(allexts[name])
  763. opts = {'label':name, 'cpath':'extensions.' + name, 'tooltip':tt,
  764. 'valfunc':self.validateextensions}
  765. w = genCheckBox(opts)
  766. w.installEventFilter(self)
  767. row, col = i / maxrows, i % maxrows
  768. grid.addWidget(w, col, row)
  769. widgets.append(w)
  770. return extsinfo, widgets
  771. def eventFilter(self, obj, event):
  772. if event.type() in (QEvent.Enter, QEvent.FocusIn):
  773. self.desctext.setHtml(obj.toolTip())
  774. if event.type() == QEvent.ToolTip:
  775. return True # tooltip is shown in self.desctext
  776. return False
  777. def addPage(self, name):
  778. for data in INFO:
  779. if name == data[0]['name']:
  780. meta, info = data
  781. break
  782. if name == 'extensions':
  783. extsinfo, widgets = self.fillExtensionsFrame()
  784. self.pages[name] = name, extsinfo, widgets
  785. else:
  786. widgets = self.fillFrame(info)
  787. self.pages[name] = name, info, widgets
  788. def readCPath(self, cpath):
  789. 'Retrieve a value from the parsed config file'
  790. # Presumes single section/key level depth
  791. section, key = cpath.split('.', 1)
  792. return self.ini.get(section, key)
  793. def loadIniFile(self, rcpath):
  794. for fn in rcpath:
  795. if os.path.exists(fn):
  796. break
  797. else:
  798. for fn in rcpath:
  799. # Try to create a file from rcpath
  800. try:
  801. f = open(fn, 'w')
  802. f.write('# Generated by TortoiseHg setting dialog\n')
  803. f.close()
  804. break
  805. except (IOError, OSError):
  806. pass
  807. else:
  808. qtlib.WarningMsgBox(_('Unable to create a Mercurial.ini file'),
  809. _('Insufficient access rights, reverting to read-only'
  810. ' mode.'), parent=self)
  811. from mercurial import config
  812. self.fn = rcpath[0]
  813. return config.config()
  814. self.fn = fn
  815. return wconfig.readfile(self.fn)
  816. def recordNewValue(self, cpath, newvalue):
  817. """Set the given value to ini; returns True if changed"""
  818. # 'newvalue' is in local encoding
  819. section, key = cpath.split('.', 1)
  820. if newvalue == self.ini.get(section, key):
  821. return False
  822. if newvalue == None:
  823. try:
  824. del self.ini[section][key]
  825. except KeyError:
  826. pass
  827. else:
  828. self.ini.set(section, key, newvalue)
  829. return True
  830. def applyChanges(self):
  831. if self.readonly:
  832. return
  833. for name, info, widgets in self.pages.values():
  834. if name == 'extensions':
  835. self.applyChangesForExtensions()
  836. else:
  837. for row, e in enumerate(info):
  838. newvalue = widgets[row].value()
  839. changed = self.recordNewValue(e.cpath, newvalue)
  840. if changed and e.restartneeded:
  841. self.restartRequested.emit(e.label)
  842. try:
  843. wconfig.writefile(self.ini, self.fn)
  844. except IOError, e:
  845. qtlib.WarningMsgBox(_('Unable to write configuration file'),
  846. str(e), parent=self)
  847. def applyChangesForExtensions(self):
  848. emitChanged = False
  849. section = 'extensions'
  850. enabledexts = hglib.enabledextensions()
  851. for chk in self.pages['extensions'][2]:
  852. if (not emitChanged) and chk.isDirty():
  853. self.restartRequested.emit(_('Extensions'))
  854. emitChanged = True
  855. key = chk.opts['label']
  856. newvalue = chk.value()
  857. if newvalue and (key in enabledexts):
  858. continue # unchanged
  859. if newvalue:
  860. self.ini.set(section, key, '')
  861. else:
  862. for cand in (key, 'hgext.%s' % key, 'hgext/%s' % key):
  863. try:
  864. del self.ini[section][cand]
  865. except KeyError:
  866. pass
  867. @pyqtSlot()
  868. def validateextensions(self):
  869. section = 'extensions'
  870. enabledexts = hglib.enabledextensions()
  871. selectedexts = set(chk.opts['label']
  872. for chk in self.pages['extensions'][2]
  873. if chk.isChecked())
  874. invalidexts = hglib.validateextensions(selectedexts)
  875. def getinival(name):
  876. if section not in self.ini:
  877. return None
  878. for cand in (name, 'hgext.%s' % name, 'hgext/%s' % name):
  879. try:
  880. return self.ini[section][cand]
  881. except KeyError:
  882. pass
  883. def changable(name):
  884. curval = getinival(name)
  885. if curval not in ('', None):
  886. # enabled or unspecified, official extensions only
  887. return False
  888. elif name in enabledexts and curval is None:
  889. # re-disabling ext is not supported
  890. return False
  891. elif name in invalidexts and name not in selectedexts:
  892. # disallow to enable bad exts, but allow to disable it
  893. return False
  894. else:
  895. return True
  896. allexts = hglib.allextensions()
  897. for chk in self.pages['extensions'][2]:
  898. name = chk.opts['label']
  899. chk.setEnabled(changable(name))
  900. invalmsg = invalidexts.get(name)
  901. if invalmsg:
  902. invalmsg = invalmsg.decode('utf-8')
  903. chk.setToolTip(invalmsg or hglib.tounicode(allexts[name]))
  904. def run(ui, *pats, **opts):
  905. return SettingsDialog(opts.get('alias') == 'repoconfig',
  906. focus=opts.get('focus'))