Read about our upcoming Code of Conduct on this issue

settings.py 40 KB
Newer Older
1
2
3
4
5
6
7
8
9
# settings.py - Configuration dialog for TortoiseHg and Mercurial
#
# Copyright 2010 Steve Borho <steve@borho.org>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

import os

10
from mercurial import ui, util, error
11

12
from tortoisehg.util import hglib, settings, paths, wconfig, i18n
Steve Borho's avatar
Steve Borho committed
13
from tortoisehg.hgqt.i18n import _
14
from tortoisehg.hgqt import qtlib, qscilib, thgrepo
15
16
17
18
19
20

from PyQt4.QtCore import *
from PyQt4.QtGui import *

# Technical Debt
#   stacked widget or pages need to be scrollable
Steve Borho's avatar
Steve Borho committed
21
#   we need a consistent icon set
Steve Borho's avatar
Steve Borho committed
22
#   connect to thgrepo.configChanged signal and refresh
23
24

_unspecstr = _('<unspecified>')
25
ENTRY_WIDTH = 300
26

27
28
class SettingsCombo(QComboBox):
    def __init__(self, parent=None, **opts):
29
        QComboBox.__init__(self, parent, toolTip=opts['tooltip'])
30
31
        self.opts = opts
        self.setEditable(opts.get('canedit', False))
32
33
        self.setValidator(opts.get('validator', None))
        self.defaults = opts.get('defaults', [])
34
        if self.defaults and self.isEditable():
35
            self.setCompleter(QCompleter(self.defaults, self))
36
        self.curvalue = None
37
        self.loaded = False
38
39
40
41
42
        if 'nohist' in opts:
            self.previous = []
        else:
            settings = opts['settings']
            slist = settings.value('settings/'+opts['cpath']).toStringList()
43
            self.previous = [s for s in slist if s]
44
        self.setFixedWidth(ENTRY_WIDTH)
45
46

    def resetList(self):
47
        self.clear()
48
        ucur = hglib.tounicode(self.curvalue)
49
        if self.opts.get('defer') and not self.loaded:
50
            if self.curvalue == None: # unspecified
51
52
                self.addItem(_unspecstr)
            else:
53
                self.addItem(ucur or '...')
54
55
            return
        self.addItem(_unspecstr)
56
        curindex = None
57
        for s in self.defaults:
58
59
            if ucur == s:
                curindex = self.count()
60
            self.addItem(s)
61
62
        if self.defaults and self.previous:
            self.insertSeparator(len(self.defaults)+1)
63
        for m in self.previous:
64
65
            if ucur == m and not curindex:
                curindex = self.count()
66
            self.addItem(m)
67
68
        if curindex is not None:
            self.setCurrentIndex(curindex)
69
        elif self.curvalue is None:
70
            self.setCurrentIndex(0)
71
        elif self.curvalue:
72
            self.addItem(ucur)
73
            self.setCurrentIndex(self.count()-1)
74
75
        else:  # empty string
            self.setEditText(ucur)
76
77

    def showPopup(self):
78
79
        if self.opts.get('defer') and not self.loaded:
            self.defaults = self.opts['defer']()
80
            self.loaded = True
81
            self.resetList()
82
83
        QComboBox.showPopup(self)

84
85
    ## common APIs for all edit widgets

86
    def setValue(self, curvalue):
87
88
89
        self.curvalue = curvalue
        self.resetList()

90
    def value(self):
91
92
        utext = self.currentText()
        if utext == _unspecstr:
93
            return None
94
        if 'nohist' in self.opts or utext in self.defaults + self.previous or not utext:
95
96
97
98
            return hglib.fromunicode(utext)
        self.previous.insert(0, utext)
        self.previous = self.previous[:10]
        settings = QSettings()
99
        settings.setValue('settings/'+self.opts['cpath'], self.previous)
100
101
102
        return hglib.fromunicode(utext)

    def isDirty(self):
103
        return self.value() != self.curvalue
104

Steve Borho's avatar
Steve Borho committed
105

106
107
class PasswordEntry(QLineEdit):
    def __init__(self, parent=None, **opts):
108
        QLineEdit.__init__(self, parent, toolTip=opts['tooltip'])
109
        self.opts = opts
110
        self.curvalue = None
111
        self.setEchoMode(QLineEdit.Password)
112
        self.setFixedWidth(ENTRY_WIDTH)
113

114
115
    ## common APIs for all edit widgets

116
    def setValue(self, curvalue):
117
        self.curvalue = curvalue
118
        if curvalue:
119
            self.setText(hglib.tounicode(curvalue))
120
121
        else:
            self.setText('')
122

123
    def value(self):
124
        utext = self.text()
125
        return utext and hglib.fromunicode(utext) or None
126
127

    def isDirty(self):
128
        return self.value() != self.curvalue
129

130
131
class FontEntry(QPushButton):
    def __init__(self, parent=None, **opts):
132
        QPushButton.__init__(self, parent, toolTip=opts['tooltip'])
133
134
135
136
137
138
        self.opts = opts
        self.curvalue = None
        self.clicked.connect(self.on_clicked)
        cpath = self.opts['cpath']
        assert cpath.startswith('tortoisehg.')
        self.fname = cpath[11:]
139
        self.setFixedWidth(ENTRY_WIDTH)
140
141

    def on_clicked(self, checked):
142
143
144
        def newFont(font):
            self.setText(font.toString())
            thgf.setFont(font)
145
        thgf = qtlib.getfont(self.fname)
146
        origfont = self.currentFont() or thgf.font()
147
        dlg = QFontDialog(self)
148
149
150
151
        dlg.currentFontChanged.connect(newFont)
        font, isok = dlg.getFont(origfont, self)
        self.setText(font.toString())
        thgf.setFont(font)
152

153
154
155
156
157
158
159
    def currentFont(self):
        """currently selected QFont if specified"""
        if not self.value():
            return None

        f = QFont()
        f.fromString(self.value())
160
        return f
161

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
    ## common APIs for all edit widgets

    def setValue(self, curvalue):
        self.curvalue = curvalue
        if curvalue:
            self.setText(hglib.tounicode(curvalue))
        else:
            self.setText(_unspecstr)

    def value(self):
        utext = self.text()
        if utext == _unspecstr:
            return None
        else:
            return hglib.fromunicode(utext)

    def isDirty(self):
        return self.value() != self.curvalue
Steve Borho's avatar
Steve Borho committed
180

Johan Samyn's avatar
Johan Samyn committed
181
182
class SettingsCheckBox(QCheckBox):
    def __init__(self, parent=None, **opts):
183
        QCheckBox.__init__(self, parent, toolTip=opts['tooltip'])
Johan Samyn's avatar
Johan Samyn committed
184
185
186
187
        self.opts = opts
        self.curvalue = None
        self.setText(opts['label'])
        self.valfunc = self.opts['valfunc']
Johan Samyn's avatar
Johan Samyn committed
188
        self.toggled.connect(self.valfunc)
Johan Samyn's avatar
Johan Samyn committed
189
190

    def setValue(self, curvalue):
191
192
        if self.curvalue == None:
            self.curvalue = curvalue
Johan Samyn's avatar
Johan Samyn committed
193
194
195
196
197
198
        self.setChecked(curvalue)

    def value(self):
        return self.isChecked()

    def isDirty(self):
199
        return self.value() != self.curvalue
Johan Samyn's avatar
Johan Samyn committed
200
201


202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def genEditCombo(opts, defaults=[]):
    opts['canedit'] = True
    opts['defaults'] = defaults
    return SettingsCombo(**opts)

def genIntEditCombo(opts):
    'EditCombo, only allows integer values'
    opts['canedit'] = True
    opts['validator'] = QIntValidator()
    return SettingsCombo(**opts)

def genPasswordEntry(opts):
    'Generate a password entry box'
    return PasswordEntry(**opts)

def genDefaultCombo(opts, defaults=[]):
Steve Borho's avatar
Steve Borho committed
218
    'user must select from a list'
219
    opts['defaults'] = defaults
220
    opts['nohist'] = True
221
222
223
    return SettingsCombo(**opts)

def genBoolCombo(opts):
Steve Borho's avatar
Steve Borho committed
224
    'true, false, unspecified'
225
    opts['defaults'] = ['True', 'False']
226
    opts['nohist'] = True
227
228
229
    return SettingsCombo(**opts)

def genDeferredCombo(opts, func):
Steve Borho's avatar
Steve Borho committed
230
    'Values retrieved from a function at popup time'
231
    opts['defer'] = func
232
    opts['nohist'] = True
233
234
    return SettingsCombo(**opts)

235
236
237
def genFontEdit(opts):
    return FontEntry(**opts)

238
239
240
241
242
243
def findDiffTools():
    return hglib.difftools(ui.ui())

def findMergeTools():
    return hglib.mergetools(ui.ui())

Johan Samyn's avatar
Johan Samyn committed
244
245
246
247
def genCheckBox(opts):
    opts['nohist'] = True
    return SettingsCheckBox(**opts)

248
249
class _fi(object):
    """Information of each field"""
250
251
    __slots__ = ('label', 'cpath', 'values', 'tooltip',
                 'restartneeded', 'globalonly')
252

253
254
    def __init__(self, label, cpath, values, tooltip,
                 restartneeded=False, globalonly=False):
255
256
257
258
        self.label = label
        self.cpath = cpath
        self.values = values
        self.tooltip = tooltip
259
260
        self.restartneeded = restartneeded
        self.globalonly = globalonly
261

262
INFO = (
Yuya Nishihara's avatar
Yuya Nishihara committed
263
({'name': 'general', 'label': 'TortoiseHg', 'icon': 'thg_logo'}, (
264
265
266
267
    _fi(_('UI Language'), 'tortoisehg.ui.language',
        (genDeferredCombo, i18n.availablelanguages),
        _('Specify your preferred user interface language (restart needed)'),
        restartneeded=True, globalonly=True),
268
    _fi(_('Three-way Merge Tool'), 'ui.merge',
269
270
271
272
273
274
275
        (genDeferredCombo, findMergeTools),
        _('Graphical merge program for resolving merge conflicts.  If left'
        ' unspecified, Mercurial will use the first applicable tool it finds'
        ' on your system or use its internal merge tool that leaves conflict'
        ' markers in place.  Chose internal:merge to force conflict markers,'
        ' internal:prompt to always select local or other, or internal:dump'
        ' to leave files in the working directory for manual merging')),
276
    _fi(_('Visual Diff Tool'), 'tortoisehg.vdiff',
277
278
279
280
281
        (genDeferredCombo, findDiffTools),
        _('Specify visual diff tool, as described in the [merge-tools]'
          ' section of your Mercurial configuration files.  If left'
          ' unspecified, TortoiseHg will use the selected merge tool.'
          ' Failing that it uses the first applicable tool it finds.')),
282
    _fi(_('Visual Editor'), 'tortoisehg.editor', genEditCombo,
283
        _('Specify the visual editor used to view files.  Format:<br>'
284
          'myeditor -flags [$FILE --num=$LINENUM][--search $SEARCH]<br><br>'
285
286
          'See <a href="%s">OpenAtLine</a>'
          % 'http://bitbucket.org/tortoisehg/thg/wiki/OpenAtLine')),
287
    _fi(_('Shell'), 'tortoisehg.shell', genEditCombo,
Steve Borho's avatar
Steve Borho committed
288
        _('Specify your preferred terminal shell application')),
289
    _fi(_('Immediate Operations'), 'tortoisehg.immediate', genEditCombo,
290
291
        _('Space separated list of shell operations you would like '
          'to be performed immediately, without user interaction. '
292
293
          'Commands are "add remove revert forget". '
          'Default: None (leave blank)')),
294
    _fi(_('Poll Frequency'), 'tortoisehg.pollfreq', genIntEditCombo,
295
296
297
        _('The period (in milliseconds) between modification time polling of '
          'key repository files, looking for changes.  Values under '
          '100ms are ignored.  Default: 500')),
298
    _fi(_('Tab Width'), 'tortoisehg.tabwidth', genIntEditCombo,
299
300
        _('Specify the number of spaces that tabs expand to in various'
        ' TortoiseHg windows.'
301
        ' Default: 0, Not expanded')),
302
    _fi(_('Max Diff Size'), 'tortoisehg.maxdiff', genIntEditCombo,
303
304
305
        _('The maximum size file (in KB) that TortoiseHg will '
        'show changes for in the changelog, status, and commit windows.'
        ' A value of zero implies no limit.  Default: 1024 (1MB)')),
306
    _fi(_('Capture stderr'), 'tortoisehg.stderrcapt', genBoolCombo,
307
308
        _('Redirect stderr to a buffer which is parsed at the end of'
        ' the process for runtime errors. Default: True')),
309
    _fi(_('Fork GUI'), 'tortoisehg.guifork', genBoolCombo,
310
        _('When running from the command line, fork a background'
311
        ' process to run graphical dialogs.  Default: True')),
312
    _fi(_('Full Path Title'), 'tortoisehg.fullpath', genBoolCombo,
313
314
        _('Show a full directory path of the repository in the dialog title'
        ' instead of just the root directory name.  Default: False')),
315
    _fi(_('Auto-resolve merges'), 'tortoisehg.autoresolve', genBoolCombo,
316
317
318
319
320
321
322
        _('Indicates whether TortoiseHg should attempt to automatically resolve'
          ' changes from both sides to the same file, and only report merge'
          ' conflicts when this is not possible. When False, all files with'
          ' changes on both sides of the merge will report as conflicting, even'
          ' if the edits are to different parts of the file. In either case,'
          ' when conflicts occur, the user will be invited to review and'
          ' resolve changes manually. Default: False.')),
323
324
    )),

Yuya Nishihara's avatar
Yuya Nishihara committed
325
({'name': 'log', 'label': _('Workbench'), 'icon': 'menulog'}, (
326
    _fi(_('Author Coloring'), 'tortoisehg.authorcolor', genBoolCombo,
327
328
329
330
        _('Color changesets by author name.  If not enabled,'
        ' the changes are colored green for merge, red for'
        ' non-trivial parents, black for normal.'
        ' Default: False')),
331
332
333
    _fi(_('Task Tabs'), 'tortoisehg.tasktabs', (genDefaultCombo,
         ['east', 'west', 'off']),
        _('Show tabs along the side of the bottom half of each repo'
334
        ' widget allowing one to switch task tabs without using the toolbar.'
335
        ' Default: off')),
336
    _fi(_('Long Summary'), 'tortoisehg.longsummary', genBoolCombo,
337
338
339
        _('If true, concatenate multiple lines of changeset summary'
        ' until they reach 80 characters.'
        ' Default: False')),
340
    _fi(_('Log Batch Size'), 'tortoisehg.graphlimit', genIntEditCombo,
341
342
343
        _('The number of revisions to read and display in the'
        ' changelog viewer in a single batch.'
        ' Default: 500')),
344
    _fi(_('Dead Branches'), 'tortoisehg.deadbranch', genEditCombo,
345
346
        _('Comma separated list of branch names that should be ignored'
        ' when building a list of branch names for a repository.'
347
        ' Default: None (leave blank)')),
348
    _fi(_('Branch Colors'), 'tortoisehg.branchcolors', genEditCombo,
349
350
351
352
353
        _('Space separated list of branch names and colors of the form'
        ' branch:#XXXXXX. Spaces and colons in the branch name must be'
        ' escaped using a backslash (\\). Likewise some other characters'
        ' can be escaped in this way, e.g. \\u0040 will be decoded to the'
        ' @ character, and \\n to a linefeed.'
354
        ' Default: None (leave blank)')),
355
    _fi(_('Hide Tags'), 'tortoisehg.hidetags', genEditCombo,
356
357
        _('Space separated list of tags that will not be shown.'
        ' Useful example: Specify "qbase qparent qtip" to hide the'
Johan Samyn's avatar
Johan Samyn committed
358
        ' standard tags inserted by the Mercurial Queues Extension.'
359
        ' Default: None (leave blank)')),
360
    _fi(_('After Pull Operation'), 'tortoisehg.postpull', (genDefaultCombo,
361
362
363
364
        ['none', 'update', 'fetch', 'rebase']),
        _('Operation which is performed directly after a successful pull.'
        ' update equates to pull --update, fetch equates to the fetch'
        ' extension, rebase equates to pull --rebase.  Default: none')),
365
366
    )),

Yuya Nishihara's avatar
Yuya Nishihara committed
367
({'name': 'commit', 'label': _('Commit'), 'icon': 'menucommit'}, (
368
    _fi(_('Username'), 'ui.username', genEditCombo,
369
        _('Name associated with commits.  The common format is<br>'
370
          '"Full Name &lt;email@example.com&gt;"')),
371
    _fi(_('Summary Line Length'), 'tortoisehg.summarylen', genIntEditCombo,
372
373
374
       _('Suggested length of commit message lines. A red vertical'
         ' line will mark this length.  CTRL-E will reflow the current'
         ' paragraph to the specified line length. Default: 80')),
375
    _fi(_('Close After Commit'), 'tortoisehg.closeci', genBoolCombo,
376
377
        _('Close the commit tool after every successful'
          ' commit.  Default: False')),
378
    _fi(_('Push After Commit'), 'tortoisehg.cipushafter', genEditCombo,
379
380
        _('Attempt to push to specified URL or alias after each successful'
          ' commit.  Default: No push')),
381
    _fi(_('Auto Commit List'), 'tortoisehg.autoinc', genEditCombo,
382
383
       _('Comma separated list of files that are automatically included'
         ' in every commit.  Intended for use only as a repository setting.'
384
         '  Default: None (leave blank)')),
385
    _fi(_('Auto Exclude List'), 'tortoisehg.ciexclude', genEditCombo,
386
       _('Comma separated list of files that are automatically unchecked'
387
         ' when the status, and commit dialogs are opened.'
388
         '  Default: None (leave blank)')),
389
    _fi(_('English Messages'), 'tortoisehg.engmsg', genBoolCombo,
390
391
392
393
394
395
       _('Generate English commit messages even if LANGUAGE or LANG'
         ' environment variables are set to a non-English language.'
         ' This setting is used by the Merge, Tag and Backout dialogs.'
         '  Default: False')),
    )),

Yuya Nishihara's avatar
Yuya Nishihara committed
396
({'name': 'web', 'label': _('Web Server'), 'icon': 'proxy'}, (
397
    _fi(_('Name'), 'web.name', genEditCombo,
398
399
        _('Repository name to use in the web interface, and by TortoiseHg '
        ' as a shorthand name.  Default is the working directory.')),
400
    _fi(_('Description'), 'web.description', genEditCombo,
401
402
        _("Textual description of the repository's purpose or"
        ' contents.')),
403
    _fi(_('Contact'), 'web.contact', genEditCombo,
404
405
        _('Name or email address of the person in charge of the'
        ' repository.')),
406
    _fi(_('Style'), 'web.style', (genDefaultCombo,
407
408
        ['paper', 'monoblue', 'coal', 'spartan', 'gitweb', 'old']),
        _('Which template map style to use')),
409
    _fi(_('Archive Formats'), 'web.allow_archive',
410
        (genEditCombo, ['bz2', 'gz', 'zip']),
411
412
        _('Comma separated list of archive formats allowed for'
        ' downloading')),
413
414
    _fi(_('Port'), 'web.port', genIntEditCombo, _('Port to listen on')),
    _fi(_('Push Requires SSL'), 'web.push_ssl', genBoolCombo,
415
416
        _('Whether to require that inbound pushes be transported'
        ' over SSL to prevent password sniffing.')),
417
    _fi(_('Stripes'), 'web.stripes', genIntEditCombo,
418
419
        _('How many lines a "zebra stripe" should span in multiline output.'
        ' Default is 1; set to 0 to disable.')),
420
    _fi(_('Max Files'), 'web.maxfiles', genIntEditCombo,
421
        _('Maximum number of files to list per changeset. Default: 10')),
422
    _fi(_('Max Changes'), 'web.maxchanges', genIntEditCombo,
423
424
        _('Maximum number of changes to list on the changelog. '
          'Default: 10')),
425
    _fi(_('Allow Push'), 'web.allow_push', (genEditCombo, ['*']),
426
427
428
429
430
431
432
        _('Whether to allow pushing to the repository. If empty or not'
        ' set, push is not allowed. If the special value "*", any remote'
        ' user can push, including unauthenticated users. Otherwise, the'
        ' remote user must have been authenticated, and the authenticated'
        ' user name must be present in this list (separated by whitespace'
        ' or ","). The contents of the allow_push list are examined after'
        ' the deny_push list.')),
433
    _fi(_('Deny Push'), 'web.deny_push', (genEditCombo, ['*']),
434
435
436
437
438
439
        _('Whether to deny pushing to the repository. If empty or not set,'
        ' push is not denied. If the special value "*", all remote users'
        ' are denied push. Otherwise, unauthenticated users are all'
        ' denied, and any authenticated user name present in this list'
        ' (separated by whitespace or ",") is also denied. The contents'
        ' of the deny_push list are examined before the allow_push list.')),
440
    _fi(_('Encoding'), 'web.encoding', (genEditCombo, ['UTF-8']),
441
442
443
        _('Character encoding name')),
    )),

Steve Borho's avatar
Steve Borho committed
444
({'name': 'proxy', 'label': _('Proxy'), 'icon': QStyle.SP_DriveNetIcon}, (
445
    _fi(_('Host'), 'http_proxy.host', genEditCombo,
446
447
        _('Host name and (optional) port of proxy server, for'
        ' example "myproxy:8000"')),
448
    _fi(_('Bypass List'), 'http_proxy.no', genEditCombo,
449
450
        _('Optional. Comma-separated list of host names that'
        ' should bypass the proxy')),
451
    _fi(_('User'), 'http_proxy.user', genEditCombo,
452
        _('Optional. User name to authenticate with at the proxy server')),
453
    _fi(_('Password'), 'http_proxy.passwd', genPasswordEntry,
454
455
456
        _('Optional. Password to authenticate with at the proxy server')),
    )),

Steve Borho's avatar
Steve Borho committed
457
({'name': 'email', 'label': _('Email'), 'icon': QStyle.SP_ArrowForward}, (
458
    _fi(_('From'), 'email.from', genEditCombo,
459
460
        _('Email address to use in the "From" header and for'
        ' the SMTP envelope')),
461
    _fi(_('To'), 'email.to', genEditCombo,
462
        _('Comma-separated list of recipient email addresses')),
463
    _fi(_('Cc'), 'email.cc', genEditCombo,
464
        _('Comma-separated list of carbon copy recipient email addresses')),
465
    _fi(_('Bcc'), 'email.bcc', genEditCombo,
466
467
        _('Comma-separated list of blind carbon copy recipient'
        ' email addresses')),
468
    _fi(_('method'), 'email.method', (genEditCombo, ['smtp']),
469
470
471
472
473
474
        _('Optional. Method to use to send email messages. If value is'
        ' "smtp" (default), use SMTP (configured below).  Otherwise, use as'
        ' name of program to run that acts like sendmail (takes "-f" option'
        ' for sender, list of recipients on command line, message on stdin).'
        ' Normally, setting this to "sendmail" or "/usr/sbin/sendmail"'
        ' is enough to use sendmail to send messages.')),
475
    _fi(_('SMTP Host'), 'smtp.host', genEditCombo,
476
        _('Host name of mail server')),
477
    _fi(_('SMTP Port'), 'smtp.port', genIntEditCombo,
478
479
        _('Port to connect to on mail server.'
        ' Default: 25')),
480
    _fi(_('SMTP TLS'), 'smtp.tls', genBoolCombo,
481
482
        _('Connect to mail server using TLS.'
        ' Default: False')),
483
    _fi(_('SMTP Username'), 'smtp.username', genEditCombo,
484
        _('Username to authenticate to mail server with')),
485
    _fi(_('SMTP Password'), 'smtp.password', genPasswordEntry,
486
        _('Password to authenticate to mail server with')),
487
    _fi(_('Local Hostname'), 'smtp.local_hostname', genEditCombo,
488
489
490
491
        _('Hostname the sender can use to identify itself to the'
        ' mail server.')),
    )),

Steve Borho's avatar
Steve Borho committed
492
493
({'name': 'diff', 'label': _('Diff'),
  'icon': QStyle.SP_FileDialogContentsView}, (
494
    _fi(_('Patch EOL'), 'patch.eol', (genDefaultCombo,
495
496
497
498
499
        ['auto', 'strict', 'crlf', 'lf']),
        _('Normalize file line endings during and after patch to lf or'
        ' crlf.  Strict does no normalization.  Auto does per-file'
        ' detection, and is the recommended setting.'
        ' Default: strict')),
500
    _fi(_('Git Format'), 'diff.git', genBoolCombo,
501
502
        _('Use git extended diff header format.'
        ' Default: False')),
503
    _fi(_('MQ Git Format'), 'mq.git', (genDefaultCombo,
504
505
506
507
508
        ['auto', 'keep', 'yes', 'no']),
     _("If set to 'keep', mq will obey the [diff] section configuration while"
       " preserving existing git patches upon qrefresh. If set to 'yes' or"
       " 'no', mq will override the [diff] section and always generate git or"
       " regular patches, possibly losing data in the second case.")),
509
    _fi(_('No Dates'), 'diff.nodates', genBoolCombo,
510
511
        _('Do not include modification dates in diff headers.'
        ' Default: False')),
512
    _fi(_('Show Function'), 'diff.showfunc', genBoolCombo,
513
514
        _('Show which function each change is in.'
        ' Default: False')),
515
    _fi(_('Ignore White Space'), 'diff.ignorews', genBoolCombo,
516
517
        _('Ignore white space when comparing lines.'
        ' Default: False')),
518
    _fi(_('Ignore WS Amount'), 'diff.ignorewsamount', genBoolCombo,
519
520
        _('Ignore changes in the amount of white space.'
        ' Default: False')),
521
    _fi(_('Ignore Blank Lines'), 'diff.ignoreblanklines', genBoolCombo,
522
523
524
        _('Ignore changes whose lines are all blank.'
        ' Default: False')),
    )),
525

Johan Samyn's avatar
Johan Samyn committed
526
({'name': 'fonts', 'label': _('Fonts'), 'icon': 'fonts'}, (
527
    _fi(_('Message Font'), 'tortoisehg.fontcomment', genFontEdit,
528
529
        _('Font used to display commit messages. Default: monospace 10'),
       globalonly=True),
530
    _fi(_('Diff Font'), 'tortoisehg.fontdiff', genFontEdit,
531
532
        _('Font used to display text differences. Default: monospace 10'),
       globalonly=True),
533
    _fi(_('List Font'), 'tortoisehg.fontlist', genFontEdit,
534
535
        _('Font used to display file lists. Default: sans 9'),
       globalonly=True),
536
    _fi(_('ChangeLog Font'), 'tortoisehg.fontlog', genFontEdit,
537
538
        _('Font used to display changelog data. Default: monospace 10'),
       globalonly=True),
539
540
541
    _fi(_('Output Font'), 'tortoisehg.fontoutputlog', genFontEdit,
        _('Font used to display output messages. Default: sans 8'),
       globalonly=True),
542
    )),
543

Johan Samyn's avatar
Johan Samyn committed
544
545
546
({'name': 'extensions', 'label': _('Extensions'), 'icon': 'extensions'}, (
    )),

547
({'name': 'reviewboard', 'label': _('Review Board'), 'icon': 'reviewboard'}, (
548
    _fi(_('Server'), 'reviewboard.server', genEditCombo,
549
550
        _('Path to review board'
        ' example "http://demo.reviewboard.org"')),
551
    _fi(_('User'), 'reviewboard.user', genEditCombo,
552
        _('User name to authenticate with review board')),
553
    _fi(_('Password'), 'reviewboard.password', genPasswordEntry,
554
        _('Password to authenticate with review board')),
555
556
    _fi(_('Server Repository ID'), 'reviewboard.repoid', genEditCombo,
        _('The default repository id for this repo on the review board server')),
557
    _fi(_('Target Groups'), 'reviewboard.target_groups', genEditCombo,
André Sintzoff's avatar
André Sintzoff committed
558
        _('A comma separated list of target groups')),
559
    _fi(_('Target People'), 'reviewboard.target_people', genEditCombo,
André Sintzoff's avatar
André Sintzoff committed
560
        _('A comma separated list of target people')),
561
562
    )),

563
564
565
566
567
568
569
)

CONF_GLOBAL = 0
CONF_REPO   = 1

class SettingsDialog(QDialog):
    'Dialog for editing Mercurial.ini or hgrc'
570
    def __init__(self, configrepo=False, focus=None, parent=None, root=None):
571
        QDialog.__init__(self, parent)
572
        self.setWindowTitle(_('TortoiseHg Settings'))
573
        self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
574

575
        if not hasattr(wconfig.config(), 'write'):
576
577
578
579
580
581
582
583
            qtlib.ErrorMsgBox(_('Iniparse package not found'),
                         _("Can't change settings without iniparse package - "
                           'view is readonly.'), parent=self)
            print 'Please install http://code.google.com/p/iniparse/'

        layout = QVBoxLayout()
        self.setLayout(layout)

584
585
586
587
        s = QSettings()
        self.settings = s
        self.restoreGeometry(s.value('settings/geom').toByteArray())

588
589
590
        def username():
            return util.username() or os.environ.get('USERNAME') or _('User')

591
592
        self.conftabs = QTabWidget()
        layout.addWidget(self.conftabs)
593
594
        utab = SettingsForm(rcpath=util.user_rcpath(), focus=focus)
        self.conftabs.addTab(utab, qtlib.geticon('settings_user'),
595
                             _("%s's global settings") % username())
596
        utab.restartRequested.connect(self._pushRestartRequest)
597

598
599
600
601
        try:
            if root is None:
                root = paths.find_root()
            if root:
Steve Borho's avatar
Steve Borho committed
602
                repo = thgrepo.repository(ui.ui(), root)
603
604
605
606
607
608
609
610
            else:
                repo = None
        except error.RepoError:
            repo = None
            if configrepo:
                qtlib.ErrorMsgBox(_('No repository found'),
                                  _('no repo at ') + root, parent=self)

611
        if repo:
612
            reporcpath = os.sep.join([repo.root, '.hg', 'hgrc'])
613
614
            rtab = SettingsForm(rcpath=reporcpath, focus=focus)
            self.conftabs.addTab(rtab, qtlib.geticon('settings_repo'),
615
                                 _('%s repository settings') % repo.displayname)
616
            rtab.restartRequested.connect(self._pushRestartRequest)
617

618
619
        BB = QDialogButtonBox
        bb = QDialogButtonBox(BB.Ok|BB.Cancel)
Steve Borho's avatar
Steve Borho committed
620
621
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
622
623
624
        layout.addWidget(bb)
        self.bb = bb

625
        self._restartreqs = set()
626

627
        self.conftabs.setCurrentIndex(configrepo and CONF_REPO or CONF_GLOBAL)
628

629
    def isDirty(self):
630
631
        return util.any(self.conftabs.widget(i).isDirty()
                        for i in xrange(self.conftabs.count()))
632

633
634
635
    @pyqtSlot(unicode)
    def _pushRestartRequest(self, key):
        self._restartreqs.add(unicode(key))
636

637
    def applyChanges(self):
638
639
        for i in xrange(self.conftabs.count()):
            self.conftabs.widget(i).applyChanges()
640
641
642
643
644
645
        if self._restartreqs:
            qtlib.InfoMsgBox(_('Settings'),
                             _('Restart all TortoiseHg applications'
                               ' for the following changes to take effect:'),
                             ', '.join(sorted(self._restartreqs)))
            self._restartreqs.clear()
646
647
648
649
650
651

    def canExit(self):
        if self.isDirty():
            ret = qtlib.CustomPrompt(_('Confirm Exit'),
                            _('Apply changes before exit?'), self,
                            (_('&Yes'), _('&No (discard changes)'),
652
                         _  ('Cancel')), default=2, esc=2).run()
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
            if ret == 2:
                return False
            elif ret == 0:
                self.applyChanges()
                return True
        return True

    def accept(self):
        self.applyChanges()
        s = self.settings
        s.setValue('settings/geom', self.saveGeometry())
        s.sync()
        QDialog.accept(self)

    def reject(self):
        if not self.canExit():
            return
        s = self.settings
        s.setValue('settings/geom', self.saveGeometry())
        s.sync()
        QDialog.reject(self)

class SettingsForm(QWidget):
    """Widget for each settings file"""

678
    restartRequested = pyqtSignal(unicode)
679

680
    def __init__(self, rcpath, focus=None, parent=None):
681
682
        super(SettingsForm, self).__init__(parent)

683
684
685
686
687
        if isinstance(rcpath, (list, tuple)):
            self.rcpath = rcpath
        else:
            self.rcpath = [rcpath]

688
689
690
        layout = QVBoxLayout()
        self.setLayout(layout)

691
692
693
        tophbox = QHBoxLayout()
        layout.addLayout(tophbox)

694
695
696
697
698
        self.fnedit = QLineEdit()
        self.fnedit.setReadOnly(True)
        self.fnedit.setFrame(False)
        self.fnedit.setFocusPolicy(Qt.NoFocus)
        self.fnedit.setStyleSheet('QLineEdit { background: transparent; }')
699
700
701
702
703
704
        edit = QPushButton(_('Edit File'))
        edit.clicked.connect(self.editClicked)
        self.editbtn = edit
        reload = QPushButton(_('Reload'))
        reload.clicked.connect(self.reloadClicked)
        self.reloadbtn = reload
705
706
        tophbox.addWidget(QLabel(_('Settings File:')))
        tophbox.addWidget(self.fnedit)
707
708
709
        tophbox.addWidget(edit)
        tophbox.addWidget(reload)

710
711
712
        bothbox = QHBoxLayout()
        layout.addLayout(bothbox)
        pageList = QListWidget()
Steve Borho's avatar
Steve Borho committed
713
        pageList.setResizeMode(QListView.Fixed)
714
715
716
        stack = QStackedWidget()
        bothbox.addWidget(pageList, 0)
        bothbox.addWidget(stack, 1)
717
        pageList.currentRowChanged.connect(stack.setCurrentIndex)
718
719
720

        self.pages = {}
        self.stack = stack
721
        self.pageList = pageList
722
723

        desctext = QTextBrowser()
724
        desctext.setOpenExternalLinks(True)
725
726
727
        layout.addWidget(desctext)
        self.desctext = desctext

728
        self.settings = QSettings()
729

730
731
        # add page items to treeview
        for meta, info in INFO:
Steve Borho's avatar
Steve Borho committed
732
            if isinstance(meta['icon'], str):
Yuya Nishihara's avatar
Yuya Nishihara committed
733
                icon = qtlib.geticon(meta['icon'])
Steve Borho's avatar
Steve Borho committed
734
735
            else:
                style = QApplication.style()
Yuya Nishihara's avatar
Yuya Nishihara committed
736
                icon = QIcon()
Steve Borho's avatar
Steve Borho committed
737
738
739
                icon.addPixmap(style.standardPixmap(meta['icon']))
            item = QListWidgetItem(icon, meta['label'])
            pageList.addItem(item)
740
741
            self.addPage(meta['name'])

742
        self.refresh()
743
        self.focusField(focus or 'ui.merge')
744

745
    def editClicked(self):
746
        'Open internal editor in stacked widget'
747
748
749
        if self.isDirty():
            ret = qtlib.CustomPrompt(_('Confirm Save'),
                    _('Save changes before edit?'), self,
750
                    (_('&Save'), _('&Discard'), _('Cancel')),
751
752
753
754
755
                    default=2, esc=2).run()
            if ret == 0:
                self.applyChanges()
            elif ret == 2:
                return
756
        if qscilib.fileEditor(self.fn, foldable=True) == QDialog.Accepted:
757
            self.refresh()
758

759
760
    def refresh(self, *args):
        # refresh config values
761
        self.ini = self.loadIniFile(self.rcpath)
762
        self.readonly = not (hasattr(self.ini, 'write') and os.access(self.fn, os.W_OK))
763
        self.stack.setDisabled(self.readonly)
764
        self.fnedit.setText(hglib.tounicode(self.fn))
Johan Samyn's avatar
Johan Samyn committed
765
766
        for name, info, widgets in self.pages.values():
            if name == 'extensions':
767
                extsmentioned = False
Johan Samyn's avatar
Johan Samyn committed
768
                for row, w in enumerate(widgets):
769
770
771
772
773
                    val = self.readCPath('extensions.' + w.opts['label'])
                    if val == None:
                        curvalue = False
                    elif len(val) and val[0] == '!':
                        curvalue = False
774
                        extsmentioned = True
775
776
                    else:
                        curvalue = True
777
                        extsmentioned = True
Johan Samyn's avatar
Johan Samyn committed
778
                    w.setValue(curvalue)
779
780
781
782
                if not extsmentioned:
                    # make sure widgets are shown properly,
                    # even when no extensions mentioned in the config file
                    self.validateextensions()
Johan Samyn's avatar
Johan Samyn committed
783
            else:
784
785
                for row, e in enumerate(info):
                    curvalue = self.readCPath(e.cpath)
Johan Samyn's avatar
Johan Samyn committed
786
                    widgets[row].setValue(curvalue)
787

788
789
790
    def isDirty(self):
        if self.readonly:
            return False
Johan Samyn's avatar
Johan Samyn committed
791
        for name, info, widgets in self.pages.values():
792
793
794
795
            for w in widgets:
                if w.isDirty():
                    return True
        return False
796

797
    def reloadClicked(self):
798
        if self.isDirty():
799
800
801
802
803
804
805
806
807
            d = QMessageBox.question(self, _('Confirm Reload'),
                            _('Unsaved changes will be lost.\n'
                            'Do you want to reload?'),
                            QMessageBox.Ok | QMessageBox.Cancel)
            if d != QMessageBox.Ok:
                return
        self.refresh()

    def focusField(self, focusfield):
808
809
        'Set page and focus to requested datum'
        for i, (meta, info) in enumerate(INFO):
810
811
            for n, e in enumerate(info):
                if e.cpath == focusfield:
812
                    self.pageList.setCurrentRow(i)
Johan Samyn's avatar
Johan Samyn committed
813
814
                    QTimer.singleShot(0, lambda:
                            self.pages[meta['name']][2][n].setFocus())
815
816
                    return

817
    def fillFrame(self, info):
818
        widgets = []
819
        frame = QFrame()
820
821
        form = QFormLayout()
        frame.setLayout(form)
822
        self.stack.addWidget(frame)
823

824
825
        for e in info:
            opts = {'label': e.label, 'cpath': e.cpath, 'tooltip': e.tooltip,
826
                    'settings':self.settings}
827
828
829
            if isinstance(e.values, tuple):
                func = e.values[0]
                w = func(opts, e.values[1])
830
            else:
831
                func = e.values
832
                w = func(opts)
833
            w.installEventFilter(self)
834
835
            if e.globalonly:
                w.setEnabled(self.rcpath == util.user_rcpath())
836
            lbl = QLabel(e.label)
837
            lbl.installEventFilter(self)
838
            lbl.setToolTip(e.tooltip)
839
            form.addRow(lbl, w)
840
841
842
            widgets.append(w)
        return widgets

Johan Samyn's avatar
Johan Samyn committed
843
844
845
846
847
848
849
850
851
852
853
854
855
    def fillExtensionsFrame(self):
        widgets = []
        frame = QFrame()
        grid = QGridLayout()
        frame.setLayout(grid)
        self.stack.addWidget(frame)
        allexts = hglib.allextensions()
        allextslist = list(allexts)
        MAXCOLUMNS = 3
        maxrows = (len(allextslist) + MAXCOLUMNS - 1) / MAXCOLUMNS
        i = 0
        extsinfo = ()
        for i, name in enumerate(sorted(allexts)):
856
            tt = hglib.tounicode(allexts[name])
Johan Samyn's avatar
Johan Samyn committed
857
858
859
            opts = {'label':name, 'cpath':'extensions.' + name, 'tooltip':tt,
                    'valfunc':self.validateextensions}
            w = genCheckBox(opts)
860
            w.installEventFilter(self)
Johan Samyn's avatar
Johan Samyn committed
861
862
863
864
865
            row, col = i / maxrows, i % maxrows
            grid.addWidget(w, col, row)
            widgets.append(w)
        return extsinfo, widgets

866
    def eventFilter(self, obj, event):
867
        if event.type() in (QEvent.Enter, QEvent.FocusIn):