1
# thgconfig.py - Configuration dialog for TortoiseHg and Mercurial
3
# Copyright 2007 Steve Borho <steve@borho.org>
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.
14
from mercurial import hg, ui, util, url, filemerge, error, extensions
16
from tortoisehg.util.i18n import _
17
from tortoisehg.util import hglib, settings, paths, i18n
19
from tortoisehg.hgtk import dialog, gdialog, gtklib, hgcmd
22
from tortoisehg.hgtk import bugtraq
23
bugtraq.get_issue_plugins_with_names()
28
from iniparse.config import Undefined
30
class Undefined(object):
33
_unspecstr = _('<unspecified>')
34
_unspeclocalstr = hglib.fromutf(_unspecstr)
36
_pwfields = ('http_proxy.passwd', 'smtp.password')
38
# options available only on global settings
39
_norepofields = ('tortoisehg.ui.language',)
42
({'name': 'general', 'label': 'TortoiseHg', 'icon': 'thg_logo.ico'}, (
43
(_('UI Language'), 'tortoisehg.ui.language', i18n.availablelanguages(),
44
_('Specify your preferred user interface language (restart needed)')),
45
(_('Three-way Merge Tool'), 'ui.merge', [],
46
_('Graphical merge program for resolving merge conflicts. If left'
47
' unspecified, Mercurial will use the first applicable tool it finds'
48
' on your system or use its internal merge tool that leaves conflict'
49
' markers in place. Chose internal:merge to force conflict markers,'
50
' internal:prompt to always select local or other, or internal:dump'
51
' to leave files in the working directory for manual merging')),
52
(_('Visual Diff Tool'), 'tortoisehg.vdiff', [],
53
_('Specify visual diff tool, as described in the [merge-tools]'
54
' section of your Mercurial configuration files. If left'
55
' unspecified, TortoiseHg will use the selected merge tool.'
56
' Failing that it uses the first applicable tool it finds.')),
57
(_('Visual Editor'), 'tortoisehg.editor', [],
58
_('Specify the visual editor used to view files, etc')),
59
(_('CLI Editor'), 'ui.editor', [],
60
_('The editor to use during a commit and other instances where'
61
' Mercurial needs multiline input from the user. Used by'
62
' command line commands, including patch import.')),
63
(_('Tab Width'), 'tortoisehg.tabwidth', [],
64
_('Specify the number of spaces that tabs expand to in various'
65
' TortoiseHg windows.'
66
' Default: Not expanded')),
67
(_('Max Diff Size'), 'tortoisehg.maxdiff', ['1024', '0'],
68
_('The maximum size file (in KB) that TortoiseHg will '
69
'show changes for in the changelog, status, and commit windows.'
70
' A value of zero implies no limit. Default: 1024 (1MB)')),
71
(_('Bottom Diffs'), 'gtools.diffbottom', ['False', 'True'],
72
_('Show the diff panel below the file list in status, shelve, and'
74
' Default: False (show diffs to right of file list)')),
75
(_('Capture stderr'), 'tortoisehg.stderrcapt', ['True', 'False'],
76
_('Redirect stderr to a buffer which is parsed at the end of'
77
' the process for runtime errors. Default: True')),
78
(_('Fork hgtk'), 'tortoisehg.hgtkfork', ['True', 'False'],
79
_('When running hgtk from the command line, fork a background'
80
' process to run graphical dialogs. Default: True')),
81
(_('Full Path Title'), 'tortoisehg.fullpath', ['False', 'True'],
82
_('Show a full directory path of the repository in the dialog title'
83
' instead of just the root directory name. Default: False')),
84
) + (gtklib.hasspellcheck() and
85
((_('Spell Check Language'), 'tortoisehg.spellcheck', [],
86
_('Default language for spell check. System language is'
87
' used if not specified. Examples: en, en_GB, en_US')),) or ())),
89
({'name': 'commit', 'label': _('Commit'), 'icon': 'menucommit.ico'}, (
90
(_('Username'), 'ui.username', [],
91
_('Name associated with commits')),
92
(_('Summary Line Length'), 'tortoisehg.summarylen', ['0', '70'],
93
_('Maximum length of the commit message summary line.'
94
' If set, TortoiseHg will issue a warning if the'
95
' summary line is too long or not separated by a'
96
' blank line. Default: 0 (unenforced)')),
97
(_('Message Line Length'), 'tortoisehg.messagewrap', ['0', '80'],
98
_('Word wrap length of the commit message. If'
99
' set, the popup menu can be used to format'
100
' the message and a warning will be issued'
101
' if any lines are too long at commit.'
102
' Default: 0 (unenforced)')),
103
(_('Close After Commit'), 'tortoisehg.closeci', ['False', 'True'],
104
_('Close the commit tool after every successful'
105
' commit. Default: False')),
106
(_('Push After Commit'), 'tortoisehg.pushafterci', ['False', 'True'],
107
_('Attempt to push to default push target after every successful'
108
' commit. Default: False')),
109
(_('Auto Commit List'), 'tortoisehg.autoinc', [],
110
_('Comma separated list of files that are automatically included'
111
' in every commit. Intended for use only as a repository setting.'
112
' Default: None (leave blank)')),
113
(_('Auto Exclude List'), 'tortoisehg.ciexclude', [],
114
_('Comma separated list of files that are automatically unchecked'
115
' when the status, commit, and shelve dialogs are opened.'
116
' Default: None (leave blank)')),
117
(_('English Messages'), 'tortoisehg.engmsg', ['False', 'True'],
118
_('Generate English commit messages even if LANGUAGE or LANG'
119
' environment variables are set to a non-English language.'
120
' This setting is used by the Merge, Tag and Backout dialogs.'
122
(_('Default Tab'), 'tortoisehg.statustab', ['0', '1', '2'],
123
_('The tab on which the status and commit tools will open.'
124
' 0 - TextDiff, 1 - Hunk Selection, 2 - Commit Preview.'
128
({'name': 'log', 'label': _('Repository Explorer'),
129
'icon': 'menulog.ico'}, (
130
(_('Author Coloring'), 'tortoisehg.authorcolor', ['False', 'True'],
131
_('Color changesets by author name. If not enabled,'
132
' the changes are colored green for merge, red for'
133
' non-trivial parents, black for normal.'
135
(_('Long Summary'), 'tortoisehg.longsummary', ['False', 'True'],
136
_('If true, concatenate multiple lines of changeset summary'
137
' until they reach 80 characters.'
139
(_('Log Batch Size'), 'tortoisehg.graphlimit', ['500'],
140
_('The number of revisions to read and display in the'
141
' changelog viewer in a single batch.'
143
(_('Dead Branches'), 'tortoisehg.deadbranch', [],
144
_('Comma separated list of branch names that should be ignored'
145
' when building a list of branch names for a repository.'
146
' Default: None (leave blank)')),
147
(_('Branch Colors'), 'tortoisehg.branchcolors', [],
148
_('Space separated list of branch names and colors of the form'
149
' branch:#XXXXXX. Spaces and colons in the branch name must be'
150
' escaped using a backslash (\\). Likewise some other characters'
151
' can be escaped in this way, e.g. \\u0040 will be decoded to the'
152
' @ character, and \\n to a linefeed.'
153
' Default: None (leave blank)')),
154
(_('Hide Tags'), 'tortoisehg.hidetags', [],
155
_('Space separated list of tags that will not be shown.'
156
' Useful example: Specify "qbase qparent qtip" to hide the'
157
' standard tags inserted by the Mercurial Queues Extension.'
158
' Default: None (leave blank)')),
159
(_('Use Expander'), 'tortoisehg.changeset-expander', ['False', 'True'],
160
_('Show changeset details with an expander')),
161
(_('Toolbar Style'), 'tortoisehg.logtbarstyle',
162
['small', 'large', 'theme'],
163
_('Adjust the display of the main toolbar in the Repository'
164
' Explorer. Values: small, large, or theme. Default: theme')),
165
# (_('F/S Encodings'), 'tortoisehg.fsencodings', [],
166
# _('Comma separated list of encodings used for filenames'
167
# ' on this computer. Default: none')),
170
({'name': 'sync', 'label': _('Synchronize'), 'icon': 'menusynch.ico',
172
(_('After Pull Operation'), 'tortoisehg.postpull',
173
['none', 'update', 'fetch', 'rebase'],
174
_('Operation which is performed directly after a successful pull.'
175
' update equates to pull --update, fetch equates to the fetch'
176
' extension, rebase equates to pull --rebase. Default: none')),
179
({'name': 'web', 'label': _('Web Server'), 'icon': 'proxy.ico'}, (
180
(_('Name'), 'web.name', ['unknown'],
181
_('Repository name to use in the web interface.'
182
' Default is the working directory.')),
183
(_('Description'), 'web.description', ['unknown'],
184
_("Textual description of the repository's purpose or"
186
(_('Contact'), 'web.contact', ['unknown'],
187
_('Name or email address of the person in charge of the'
189
(_('Style'), 'web.style',
190
['paper', 'monoblue', 'coal', 'spartan', 'gitweb', 'old'],
191
_('Which template map style to use')),
192
(_('Archive Formats'), 'web.allow_archive', ['bz2', 'gz', 'zip'],
193
_('Comma separated list of archive formats allowed for'
195
(_('Port'), 'web.port', ['8000'], _('Port to listen on')),
196
(_('Push Requires SSL'), 'web.push_ssl', ['True', 'False'],
197
_('Whether to require that inbound pushes be transported'
198
' over SSL to prevent password sniffing.')),
199
(_('Stripes'), 'web.stripes', ['1', '0'],
200
_('How many lines a "zebra stripe" should span in multiline output.'
201
' Default is 1; set to 0 to disable.')),
202
(_('Max Files'), 'web.maxfiles', ['10'],
203
_('Maximum number of files to list per changeset.')),
204
(_('Max Changes'), 'web.maxchanges', ['10'],
205
_('Maximum number of changes to list on the changelog.')),
206
(_('Allow Push'), 'web.allow_push', ['*'],
207
_('Whether to allow pushing to the repository. If empty or not'
208
' set, push is not allowed. If the special value "*", any remote'
209
' user can push, including unauthenticated users. Otherwise, the'
210
' remote user must have been authenticated, and the authenticated'
211
' user name must be present in this list (separated by whitespace'
212
' or ","). The contents of the allow_push list are examined after'
213
' the deny_push list.')),
214
(_('Deny Push'), 'web.deny_push', ['*'],
215
_('Whether to deny pushing to the repository. If empty or not set,'
216
' push is not denied. If the special value "*", all remote users'
217
' are denied push. Otherwise, unauthenticated users are all'
218
' denied, and any authenticated user name present in this list'
219
' (separated by whitespace or ",") is also denied. The contents'
220
' of the deny_push list are examined before the allow_push list.')),
221
(_('Encoding'), 'web.encoding', ['UTF-8'],
222
_('Character encoding name')),
225
({'name': 'proxy', 'label': _('Proxy'), 'icon': 'general.ico'}, (
226
(_('Host'), 'http_proxy.host', [],
227
_('Host name and (optional) port of proxy server, for'
228
' example "myproxy:8000"')),
229
(_('Bypass List'), 'http_proxy.no', [],
230
_('Optional. Comma-separated list of host names that'
231
' should bypass the proxy')),
232
(_('User'), 'http_proxy.user', [],
233
_('Optional. User name to authenticate with at the proxy server')),
234
(_('Password'), 'http_proxy.passwd', [],
235
_('Optional. Password to authenticate with at the proxy server')),
238
({'name': 'email', 'label': _('Email'), 'icon': gtk.STOCK_GOTO_LAST}, (
239
(_('From'), 'email.from', [],
240
_('Email address to use in the "From" header and for'
241
' the SMTP envelope')),
242
(_('To'), 'email.to', [],
243
_('Comma-separated list of recipient email addresses')),
244
(_('Cc'), 'email.cc', [],
245
_('Comma-separated list of carbon copy recipient email addresses')),
246
(_('Bcc'), 'email.bcc', [],
247
_('Comma-separated list of blind carbon copy recipient'
248
' email addresses')),
249
(_('method'), 'email.method', ['smtp'],
250
_('Optional. Method to use to send email messages. If value is'
251
' "smtp" (default), use SMTP (configured below). Otherwise, use as'
252
' name of program to run that acts like sendmail (takes "-f" option'
253
' for sender, list of recipients on command line, message on stdin).'
254
' Normally, setting this to "sendmail" or "/usr/sbin/sendmail"'
255
' is enough to use sendmail to send messages.')),
256
(_('SMTP Host'), 'smtp.host', [], _('Host name of mail server')),
257
(_('SMTP Port'), 'smtp.port', ['25'],
258
_('Port to connect to on mail server.'
260
(_('SMTP TLS'), 'smtp.tls', ['False', 'True'],
261
_('Connect to mail server using TLS.'
263
(_('SMTP Username'), 'smtp.username', [],
264
_('Username to authenticate to mail server with')),
265
(_('SMTP Password'), 'smtp.password', [],
266
_('Password to authenticate to mail server with')),
267
(_('Local Hostname'), 'smtp.local_hostname', [],
268
_('Hostname the sender can use to identify itself to the'
272
({'name': 'diff', 'label': _('Diff'), 'icon': gtk.STOCK_JUSTIFY_FILL}, (
273
(_('Patch EOL'), 'patch.eol', ['auto', 'strict', 'crlf', 'lf'],
274
_('Normalize file line endings during and after patch to lf or'
275
' crlf. Strict does no normalization. Auto does per-file'
276
' detection, and is the recommended setting.'
277
' Default: strict')),
278
(_('Git Format'), 'diff.git', ['False', 'True'],
279
_('Use git extended diff header format.'
281
(_('No Dates'), 'diff.nodates', ['False', 'True'],
282
_('Do not include modification dates in diff headers.'
284
(_('Show Function'), 'diff.showfunc', ['False', 'True'],
285
_('Show which function each change is in.'
287
(_('Ignore White Space'), 'diff.ignorews', ['False', 'True'],
288
_('Ignore white space when comparing lines.'
290
(_('Ignore WS Amount'), 'diff.ignorewsamount', ['False', 'True'],
291
_('Ignore changes in the amount of white space.'
293
(_('Ignore Blank Lines'), 'diff.ignoreblanklines', ['False', 'True'],
294
_('Ignore changes whose lines are all blank.'
296
(_('Coloring Style'), 'tortoisehg.diffcolorstyle',
297
['none', 'foreground', 'background'],
298
_('Adjust the coloring style of diff lines in the changeset'
299
' viewer. Default: foreground')),
302
({'name': 'font', 'label': _('Font'), 'icon': gtk.STOCK_SELECT_FONT,
303
'extra': True, 'width': 16}, (
304
(_('Commit Message'), 'gtools.fontcomment', [],
305
_('Font used in changeset viewer and commit log text.'
306
' Default: monospace 10')),
307
(_('Diff Text'), 'gtools.fontdiff', [],
308
_('Font used for diffs in status and commit tools.'
309
' Default: monospace 10')),
310
(_('File List'), 'gtools.fontlist', [],
311
_('Font used in file lists in status and commit tools.'
312
' Default: sans 9')),
313
(_('Command Output'), 'gtools.fontlog', [],
314
_('Font used in command output window.'
315
' Default: monospace 10')),
318
({'name': 'extensions', 'label': _('Extensions'), 'icon': gtk.STOCK_EXECUTE,
321
({'name': 'issue', 'label': _('Issue Tracking'), 'icon': gtk.STOCK_EDIT,
323
(_('Issue Regex'), 'tortoisehg.issue.regex', [],
324
_('Defines the regex to match when picking up issue numbers.')),
325
(_('Issue Link'), 'tortoisehg.issue.link', [],
326
_('Defines the command to run when an issue number is recognized. '
327
'You may include groups in issue.regex, and corresponding {n} '
328
'tokens in issue.link (where n is a non-negative integer). '
329
'{0} refers to the entire string matched by issue.regex, '
330
'while {1} refers to the first group and so on. If no {n} tokens'
331
'are found in issue.link, the entire matched string is appended '
333
(_('Mandatory Issue Reference'), 'tortoisehg.issue.linkmandatory',
335
_('When commiting an issue, force the user to specify a reference '
337
'If enabled, the regex configured in \'Issue Regex\' must find a '
338
'match in the commit message')),
343
'win-ja': (_('Japanese on Windows'), {
344
'gtools.fontcomment': 'MS Gothic 11',
345
'gtools.fontdiff': 'MS Gothic 10',
346
'gtools.fontlist': 'MS UI Gothic 9',
347
'gtools.fontlog': 'MS Gothic 10'})}
349
class PathEditDialog(gtk.Dialog):
350
_protocols = (('ssh', _('ssh')), ('http', _('http')),
351
('https', _('https')), ('local', _('local')))
353
def __init__(self, path, alias, list):
354
gtk.Dialog.__init__(self, parent=None, flags=gtk.DIALOG_MODAL,
355
buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
356
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
357
gtklib.set_tortoise_keys(self)
358
self.connect('response', self.response)
359
self.connect('key-press-event', self.key_press)
360
self.set_title(_('Edit remote repository path'))
361
self.set_has_separator(False)
362
self.set_resizable(False)
363
self.newpath, self.newalias = None, None
367
# Tuple: (internal name, translated name)
368
for name in (('URL', _('URL')), ('Port', _('Port')),
369
('Folder', _('Folder')), ('Host', _('Host')),
370
('User', _('User')), ('Password', _('Password')),
371
('Alias', _('Alias'))):
373
entry.set_alignment(0)
374
label = gtk.Label(name[1])
375
label.set_alignment(1, 0.5)
376
self.entries[name[0]] = [entry, label, None]
378
# persistent settings
379
self.settings = settings.Settings('pathedit')
381
# configure individual widgets
382
self.entries['Alias'][0].set_width_chars(18)
383
self.entries['URL'][0].set_width_chars(60)
384
self.entries['Port'][0].set_width_chars(8)
385
self.entries['User'][0].set_width_chars(18)
386
self.entries['Password'][0].set_width_chars(24)
387
self.entries['Password'][0].set_visibility(False)
389
# table for main entries
390
toptable = gtklib.LayoutTable()
391
self.vbox.pack_start(toptable, False, False, 2)
393
## alias (and 'Browse...' button)
394
browse = gtk.Button(_('Browse...'))
395
browse.connect('clicked', self.browse_clicked)
396
ealias = self.entries['Alias']
397
toptable.add_row(ealias[1], ealias[0], None, browse, expand=0)
400
eurl = self.entries['URL']
401
toptable.add_row(eurl[1], eurl[0], padding=False)
403
self.expander = expander = gtk.Expander(_('URL Details'))
404
self.vbox.pack_start(expander, True, True, 2)
406
# table for separated entries
407
entrytable = gtklib.LayoutTable()
408
expander.add(entrytable)
411
self.protocolcombo = gtk.combo_box_new_text()
412
for name, label in self._protocols:
413
self.protocolcombo.append_text(label)
414
toptable.add_row(_('Type'), self.protocolcombo)
417
ehost, eport = self.entries['Host'], self.entries['Port']
418
entrytable.add_row(ehost[1], ehost[0], 8, eport[1], eport[0])
421
efolder = self.entries['Folder']
422
entrytable.add_row(efolder[1], efolder[0], padding=False)
424
## username & password
425
euser, epasswd = self.entries['User'], self.entries['Password']
426
entrytable.add_row(euser[1], euser[0], 8, epasswd[1], epasswd[0])
429
group = gtklib.LayoutGroup()
430
group.add(toptable, entrytable)
434
self.setentries(path, alias)
436
self.lastproto = None
437
self.update_sensitive()
440
def protocolindex(self, pname):
441
for (i, (name, label)) in enumerate(self._protocols):
446
def protocolname(self, plabel):
447
for (name, label) in self._protocols:
452
def sethandlers(self, enable=True):
455
self.pcombo_hid = self.protocolcombo.connect('changed', self.changed)
458
if h and self.protocolcombo.handler_is_connected(h):
459
self.protocolcombo.disconnect(h)
462
for n, (e, l, h) in self.entries.iteritems():
464
handler = (n == 'URL' and self.changedurl or self.changed)
465
self.entries[n][2] = e.connect('changed', handler)
467
if e.handler_is_connected(h):
470
def urlparse(self, path):
471
m = re.match(r'^ssh://(([^@]+)@)?([^:/]+)(:(\d+))?(/(.*))?$', path)
476
folder = m.group(7) or '.'
479
elif path.startswith('http://') or path.startswith('https://'):
480
snpaqf = urlparse.urlparse(path)
481
scheme, netloc, folder, params, query, fragment = snpaqf
482
host, port, user, passwd = url.netlocsplit(netloc)
483
if folder.startswith('/'): folder = folder[1:]
485
user, host, port, passwd = [''] * 4
488
return user, host, port, folder, passwd, scheme
490
def setentries(self, path, alias=None):
492
alias = self.entries['Alias'][0].get_text()
494
user, host, port, folder, pw, scheme = self.urlparse(path)
496
self.entries['Alias'][0].set_text(alias)
497
if scheme == 'local':
498
self.entries['URL'][0].set_text(path)
500
self.entries['URL'][0].set_text(url.hidepassword(path))
501
self.entries['User'][0].set_text(user or '')
502
self.entries['Host'][0].set_text(host or '')
503
self.entries['Port'][0].set_text(port or '')
504
self.entries['Folder'][0].set_text(folder or '')
505
self.entries['Password'][0].set_text(pw or '')
507
self.protocolcombo.set_active(self.protocolindex(scheme) or 0)
509
def update_sensitive(self):
510
proto = self.protocolname(self.protocolcombo.get_active_text())
511
if proto == self.lastproto:
513
self.lastproto = proto
515
for n in ('User', 'Password', 'Port', 'Host'):
516
self.entries[n][0].set_sensitive(False)
517
self.entries[n][1].set_sensitive(False)
519
for n in ('User', 'Port', 'Host'):
520
self.entries[n][0].set_sensitive(True)
521
self.entries[n][1].set_sensitive(True)
522
self.entries['Password'][0].set_sensitive(False)
523
self.entries['Password'][1].set_sensitive(False)
525
for n in ('User', 'Password', 'Port', 'Host'):
526
self.entries[n][0].set_sensitive(True)
527
self.entries[n][1].set_sensitive(True)
529
def load_settings(self):
530
expanded = self.settings.get_value('expanded', False, True)
531
self.expander.set_property('expanded', expanded)
533
def store_settings(self):
534
expanded = self.expander.get_property('expanded')
535
self.settings.set_value('expanded', expanded)
536
self.settings.write()
538
def browse_clicked(self, button):
539
if self.protocolname(self.protocolcombo.get_active_text()) == 'local':
540
initial = self.entries['URL'][0].get_text()
543
path = gtklib.NativeFolderSelectDialog(initial=initial,
544
title=_('Select Local Folder')).run()
546
self.entries['URL'][0].set_text(path)
548
def changed(self, combo):
549
newurl = self.buildurl()
550
self.sethandlers(False)
551
self.entries['URL'][0].set_text(url.hidepassword(newurl))
552
self.sethandlers(True)
553
self.update_sensitive()
555
def changedurl(self, combo):
556
self.sethandlers(False)
557
self.setentries(self.entries['URL'][0].get_text())
558
self.sethandlers(True)
559
self.update_sensitive()
561
def response(self, widget, response_id):
562
if response_id != gtk.RESPONSE_OK:
563
self.store_settings()
566
aliasinput = self.entries['Alias'][0]
567
newalias = aliasinput.get_text()
569
ret = dialog.error_dialog(self, _('Alias name is empty'),
570
_('Please enter alias name'))
571
aliasinput.grab_focus()
573
if newalias in self.list:
574
ret = gdialog.Confirm(_('Confirm Overwrite'), [], self,
575
_("Overwrite existing '%s' path?") % newalias).run()
576
if ret != gtk.RESPONSE_YES:
578
self.newpath = self.buildurl()
579
self.newalias = newalias
580
self.store_settings()
583
def key_press(self, widget, event):
584
if event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
585
self.response(widget, gtk.RESPONSE_OK)
588
proto = self.protocolname(self.protocolcombo.get_active_text())
589
host = self.entries['Host'][0].get_text()
590
port = self.entries['Port'][0].get_text()
591
folder = self.entries['Folder'][0].get_text()
592
user = self.entries['User'][0].get_text()
593
pwd = self.entries['Password'][0].get_text()
602
elif proto == 'local':
606
netloc = url.netlocunsplit(host, port, user, pwd)
607
ret += netloc + '/' + folder
613
class ConfigDialog(gtk.Dialog):
614
def __init__(self, configrepo=False, focus=None):
615
""" Initialize the Dialog. """
616
gtk.Dialog.__init__(self, parent=None, flags=0,
617
buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
618
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
619
gtk.STOCK_APPLY, gtk.RESPONSE_APPLY))
620
gtklib.set_tortoise_keys(self)
621
self._btn_apply = self.action_area.get_children()[0]
622
self.set_has_separator(False)
623
self.set_default_size(-1, 500)
627
root = paths.find_root()
629
repo = hg.repository(self.ui, root)
630
name = hglib.get_reponame(repo)
635
except error.RepoError:
638
dialog.error_dialog(self, _('No repository found'),
639
_('no repo at ') + root)
646
self.readonly = False
648
dialog.error_dialog(self, _('Iniparse package not found'),
649
_("Can't change settings without iniparse package - "
650
'view is readonly.'))
651
print 'Please install http://code.google.com/p/iniparse/'
655
self.connect('response', self.should_live)
656
self.connect('thg-accept', self.thgaccept)
657
self.connect('delete-event', self.delete_event)
659
# wrapper box for padding
661
wrapbox.set_border_width(5)
662
self.vbox.pack_start(wrapbox)
664
# create combo to select the target
666
wrapbox.pack_start(hbox, False, False, 1)
667
combo = gtk.combo_box_new_text()
668
hbox.pack_start(combo, False, False)
669
combo.append_text(_('User global settings'))
671
combo.append_text(_('%s repository settings') % hglib.toutf(name))
672
combo.connect('changed', self.fileselect)
673
self.confcombo = combo
676
edit = gtk.Button(_('Edit File'))
677
hbox.pack_start(edit, False, False, 6)
678
edit.connect('clicked', self.edit_clicked)
680
reload = gtk.Button(_('Reload'))
681
hbox.pack_start(reload, False, False)
682
reload.connect('clicked', self.reload_clicked)
684
# insert padding of between combo and middle pane
685
wrapbox.pack_start(gtk.VBox(), False, False, 4)
687
# hbox for splitting treeview and main pane (notebook + desc)
689
wrapbox.pack_start(sidebox)
691
# create treeview for selecting configuration page
692
self.confmodel = gtk.ListStore(gtk.gdk.Pixbuf, # icon
695
self.confview = gtk.TreeView(self.confmodel)
696
self.confview.set_headers_visible(False)
697
self.confview.set_size_request(140, -1)
698
self.confview.connect('cursor-changed', self.cursor_changed)
700
tvframe = gtk.Frame()
701
sidebox.pack_start(tvframe, False, False)
702
tvframe.set_shadow_type(gtk.SHADOW_IN)
703
tvframe.add(self.confview)
705
col = gtk.TreeViewColumn()
706
self.confview.append_column(col)
707
cell = gtk.CellRendererPixbuf()
708
col.pack_start(cell, True)
709
col.add_attribute(cell, 'pixbuf', 0)
711
col = gtk.TreeViewColumn(None, gtk.CellRendererText(), text=1)
712
self.confview.append_column(col)
714
# insert padding of between treeview and main pane
715
sidebox.pack_start(gtk.HBox(), False, False, 4)
717
# vbox for splitting notebook and desc frame
719
sidebox.pack_start(mainbox)
721
# create a new notebook, place the position of the tabs
722
self.notebook = notebook = gtk.Notebook()
723
notebook.set_show_tabs(False)
724
notebook.set_show_border(False)
726
mainbox.pack_start(notebook)
730
self.tooltips = gtklib.Tooltips()
731
self.history = settings.Settings('thgconfig')
733
# add page items to treeview
734
for meta, info in INFO:
735
if meta['name'] == 'issue' and bugtraq is None:
737
pixbuf = gtklib.get_icon_pixbuf(meta['icon'],
738
gtk.ICON_SIZE_LARGE_TOOLBAR)
739
self.confmodel.append((pixbuf, meta['label'], meta['name']))
741
# insert padding of between notebook and common desc frame
742
mainbox.pack_start(gtk.VBox(), False, False, 2)
744
# common description frame
745
descframe = gtk.Frame(_('Description'))
746
mainbox.pack_start(descframe, False, False)
747
desctext = gtk.TextView()
748
desctext.set_wrap_mode(gtk.WRAP_WORD)
749
desctext.set_editable(False)
750
desctext.set_sensitive(False)
751
scrolled = gtk.ScrolledWindow()
752
scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
753
scrolled.set_border_width(4)
754
scrolled.add(desctext)
755
descframe.add(scrolled)
756
self.descbuffer = desctext.get_buffer()
758
# Force dialog into clean state in the beginning
759
self._btn_apply.set_sensitive(False)
761
combo.set_active(configrepo and CONF_REPO or CONF_GLOBAL)
763
# focus 'general' page or specified field
765
self.focus_field(focus)
767
self.show_page('general')
768
self.confview.grab_focus()
770
def fileselect(self, combo):
771
'select another hgrc file'
773
ret = gdialog.CustomPrompt(_('Confirm Switch'),
774
_('Switch after saving changes?'), self,
775
(_('&Save'), _('&Discard'), _('&Cancel')),
776
default=2, esc=2).run()
778
combo.handler_block_by_func(self.fileselect)
779
repo = combo.get_active() == CONF_GLOBAL
780
combo.set_active(repo and CONF_REPO or CONF_GLOBAL)
781
combo.handler_unblock_by_func(self.fileselect)
788
# determine target config file
789
if self.confcombo.get_active() == CONF_REPO:
790
repo = hg.repository(ui.ui(), self.root)
791
name = hglib.get_reponame(repo)
792
self.rcpath = [os.sep.join([repo.root, '.hg', 'hgrc'])]
793
self.set_title(_('TortoiseHg Configure Repository - ') + \
795
gtklib.set_tortoise_icon(self, 'settings_repo.ico')
797
self.rcpath = util.user_rcpath()
798
self.set_title(_('TortoiseHg Configure User-Global Settings'))
799
gtklib.set_tortoise_icon(self, 'settings_user.ico')
801
# refresh config values
802
self.ini = self.load_config(self.rcpath)
805
# clear modification status
806
self._btn_apply.set_sensitive(False)
809
def edit_clicked(self, button):
810
# reload configs, in case they have been written since opened
811
if self.confcombo.get_active() == CONF_REPO:
812
repo = hg.repository(ui.ui(), self.root)
816
# open config file with visual editor
817
if not gtklib.open_with_editor(u, self.fn, self):
818
self.focus_field('tortoisehg.editor')
820
def reload_clicked(self, button):
822
ret = gdialog.Confirm(_('Confirm Reload'), [], self,
823
_('Unsaved changes will be lost.\n'
824
'Do you want to reload?')).run()
825
if ret != gtk.RESPONSE_YES:
829
def delete_event(self, dlg, event):
832
def thgaccept(self, dlg):
833
self.response(gtk.RESPONSE_OK)
835
def should_live(self, dialog=None, resp=None):
836
if resp == gtk.RESPONSE_APPLY:
838
self.emit_stop_by_name('response')
840
elif resp == gtk.RESPONSE_CANCEL:
842
if self.dirty and not self.readonly:
843
if resp == gtk.RESPONSE_OK:
846
ret = gdialog.CustomPrompt(_('Confirm Exit'),
847
_('Exit after saving changes?'), self,
848
(_('&Yes'), _('&No (discard changes)'),
849
_('&Cancel')), default=2, esc=2).run()
852
self.emit_stop_by_name('response')
858
def cursor_changed(self, treeview):
859
path, focus = self.confview.get_cursor()
862
name = self.confmodel[path][2]
863
if not self.pages.has_key(name):
865
page_num, info, vbox, widgets = self.pages[name]
866
self.notebook.set_current_page(page_num)
868
def show_page(self, name):
869
'''Show page by activating treeview item'''
870
for row in self.confmodel:
876
self.confview.set_cursor(path)
878
def focus_field(self, focusfield):
879
'''Set page and focus to requested datum'''
880
for meta, info in INFO:
881
for n, (label, cpath, values, tip) in enumerate(info):
882
if cpath == focusfield:
885
widgets = self.pages[name][3]
886
widgets[n].grab_focus()
889
def new_path(self, newpath, alias='new'):
890
'''Add a new path to [paths], give default name, focus'''
891
# This method may be called from hgtk.sync, so ensure page is visible
892
self.show_page('sync')
893
i = self.pathdata.insert_before(None, None)
894
safepath = url.hidepassword(newpath)
895
if alias in [row[0] for row in self.pathdata]:
897
while len([row for row in self.pathdata if row[0] == alias]) > 0:
899
alias = 'new_%d' % num
900
self.pathdata.set_value(i, 0, alias)
901
self.pathdata.set_value(i, 1, '%s' % hglib.toutf(safepath))
902
self.pathdata.set_value(i, 2, '%s' % hglib.toutf(newpath))
903
self.pathtree.get_selection().select_iter(i)
904
self.pathtree.set_cursor(
905
self.pathdata.get_path(i),
906
self.pathtree.get_column(0))
907
self.refresh_path_list()
910
def dirty_event(self, *args):
912
self._btn_apply.set_sensitive(not self.readonly)
915
def refresh_path_list(self):
916
"""Update sensitivity of buttons"""
917
selection = self.pathtree.get_selection()
918
path_selected = (len(self.pathdata) > 0
919
and selection.count_selected_rows() > 0)
920
repo_available = self.root is not None
922
model, path = selection.get_selected()
923
default_path = model[path][0] == 'default'
926
self.editpathbtn.set_sensitive(path_selected)
927
self.delpathbtn.set_sensitive(path_selected)
928
self.testpathbtn.set_sensitive(repo_available and path_selected)
929
self.defaultpathbtn.set_sensitive(not default_path and path_selected)
931
def fill_sync_frame(self, parent, table):
933
parent.pack_start(table, False, False)
936
parent.pack_start(gtk.VBox(), False, False, 2)
939
frame = gtk.Frame(_('Remote repository paths'))
940
parent.pack_start(frame, True, True, 2)
942
vbox.set_border_width(4)
945
# Initialize data model for 'Paths' tab
946
self.pathdata = gtk.ListStore(str, str, str)
948
# Define view model for 'Paths' tab
949
self.pathtree = gtk.TreeView(self.pathdata)
950
self.pathtree.set_enable_search(False)
951
self.pathtree.add_events(gtk.gdk.BUTTON_PRESS_MASK)
953
renderer = gtk.CellRendererText()
954
column = gtk.TreeViewColumn(_('Alias'), renderer, text=0)
955
self.pathtree.append_column(column)
957
renderer = gtk.CellRendererText()
958
column = gtk.TreeViewColumn(_('Repository Path'), renderer, text=1)
959
self.pathtree.append_column(column)
961
scrolled = gtk.ScrolledWindow()
962
scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
963
scrolled.add(self.pathtree)
965
treeframe = gtk.Frame()
966
treeframe.set_border_width(2)
967
treeframe.set_shadow_type(gtk.SHADOW_IN)
968
treeframe.add(scrolled)
971
# bottom command buttons
972
bottombox = gtk.HBox()
973
vbox.pack_start(bottombox, False, False, 4)
975
addpathbtn = gtk.Button(_('_Add'))
976
addpathbtn.set_use_underline(True)
977
bottombox.pack_start(addpathbtn, True, True, 2)
979
self.editpathbtn = gtk.Button(_('_Edit'))
980
self.editpathbtn.set_use_underline(True)
981
bottombox.pack_start(self.editpathbtn, True, True, 2)
983
self.delpathbtn = gtk.Button(_('_Remove'))
984
self.delpathbtn.set_use_underline(True)
985
bottombox.pack_start(self.delpathbtn, True, True, 2)
987
self.testpathbtn = gtk.Button(_('_Test'))
988
self.testpathbtn.set_use_underline(True)
989
bottombox.pack_start(self.testpathbtn, True, True, 2)
991
self.defaultpathbtn = gtk.Button(_('Set as _default'))
992
self.defaultpathbtn.set_use_underline(True)
993
bottombox.pack_start(self.defaultpathbtn, True, True, 2)
996
def pathtree_changed(sel):
997
self.refresh_path_list()
998
def pathtree_pressed(widget, event):
999
if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS:
1000
x, y = int(event.x), int(event.y)
1001
pathinfo = self.pathtree.get_path_at_pos(x, y)
1002
if pathinfo is not None:
1004
elif event.button == 1:
1005
selection = self.pathtree.get_selection()
1006
selection.unselect_all()
1007
self.refresh_path_list()
1008
def edit_path(*args, **opts):
1009
selection = self.pathtree.get_selection()
1010
if not selection.count_selected_rows():
1012
model, path = selection.get_selected()
1013
dialog = PathEditDialog(model[path][2], model[path][0],
1014
[p[0] for p in self.pathdata if p[0] != model[path][0]])
1015
dialog.set_transient_for(self)
1018
if model[path][0] != dialog.newalias:
1019
# remove existing path
1020
rows = [row for row in model if row[0] == dialog.newalias]
1022
del model[rows[0].iter]
1024
model[path][0] = dialog.newalias
1025
model[path][1] = url.hidepassword(dialog.newpath)
1026
model[path][2] = dialog.newpath
1028
elif opts.has_key('new') and opts['new'] == True:
1029
del self.pathdata[path]
1030
self.refresh_path_list()
1033
def add_path(*args):
1034
self.new_path('http://')
1036
def remove_path(*args):
1037
selection = self.pathtree.get_selection()
1038
if not selection.count_selected_rows():
1040
model, path = selection.get_selected()
1041
next_iter = self.pathdata.iter_next(path)
1042
del self.pathdata[path]
1044
selection.select_iter(next_iter)
1045
elif len(self.pathdata):
1046
selection.select_path(len(self.pathdata) - 1)
1047
self.refresh_path_list()
1049
def test_path(*args):
1050
selection = self.pathtree.get_selection()
1051
if not selection.count_selected_rows():
1054
dialog.error_dialog(self, _('No Repository Found'),
1055
_('Path testing cannot work without a repository'))
1057
model, path = selection.get_selected()
1058
testpath = hglib.fromutf(model[path][2])
1061
if testpath[0] == '~':
1062
testpath = os.path.expanduser(testpath)
1063
cmdline = ['hg', 'incoming', '--verbose', '--', testpath]
1064
dlg = hgcmd.CmdDialog(cmdline, text='hg incoming')
1067
def make_default(*args):
1068
selection = self.pathtree.get_selection()
1069
if not selection.count_selected_rows():
1071
model, path = selection.get_selected()
1072
if model[path][0] == 'default':
1074
# collect rows has 'default' alias
1075
rows = [row for row in model if row[0] == 'default']
1077
ret = gdialog.Confirm(_('Confirm Overwrite'), [], self,
1078
_("Overwrite existing '%s' path?") % 'default').run()
1079
if ret != gtk.RESPONSE_YES:
1081
# remove old default path
1082
default_iter = rows[0].iter
1083
del model[default_iter]
1084
# set 'default' alias to selected path
1085
model[path][0] = 'default'
1086
self.refresh_path_list()
1090
self.pathtree.connect('cursor-changed', pathtree_changed)
1091
self.pathtree.connect('button-press-event', pathtree_pressed)
1092
addpathbtn.connect('clicked', add_path)
1093
self.editpathbtn.connect('clicked', edit_path)
1094
self.delpathbtn.connect('clicked', remove_path)
1095
self.testpathbtn.connect('clicked', test_path)
1096
self.defaultpathbtn.connect('clicked', make_default)
1098
def refresh_sync_frame(self):
1099
self.pathdata.clear()
1100
if 'paths' in self.ini:
1101
for name in self.ini['paths']:
1102
path = self.ini['paths'][name]
1103
safepath = hglib.toutf(url.hidepassword(path))
1104
self.pathdata.append([hglib.toutf(name), safepath,
1106
self.refresh_path_list()
1108
def fill_font_frame(self, parent, table):
1110
layout = gtklib.LayoutTable()
1111
parent.pack_start(layout, False, False, 2)
1112
parent.reorder_child(layout, 0)
1115
defaultradio = gtk.RadioButton(None, _('Theme default fonts'))
1116
presetradio = gtk.RadioButton(defaultradio, _('Preset fonts:'))
1117
customradio = gtk.RadioButton(defaultradio, _('Custom fonts:'))
1118
self.defaultradio = defaultradio
1119
self.presetradio = presetradio
1120
self.customradio = customradio
1123
presetmodel = gtk.ListStore(str, # internal name
1125
presetmodel.append((None, _(' - Select Preset -')))
1126
for name, (label, preset) in _font_presets.items():
1127
presetmodel.append((name, label))
1128
presetcombo = gtk.ComboBox(presetmodel)
1129
cell = gtk.CellRendererText()
1130
presetcombo.pack_start(cell)
1131
presetcombo.add_attribute(cell, 'text', 1)
1132
self.presetcombo = presetcombo
1135
layout.add_row(defaultradio)
1136
layout.add_row(presetradio, presetcombo)
1138
vbox.pack_start(customradio, False, False, 4)
1139
vbox.pack_start(gtk.VBox())
1140
layout.add_row(vbox, table, yhopt=gtk.FILL|gtk.EXPAND)
1143
def combo_changed(combo):
1144
# configure for each item
1145
model = combo.get_model()
1146
name = model[combo.get_active()][0] # internal name
1149
preset = _font_presets[name][1] # preset data
1150
pagenum, info, vbox, widgets = self.pages['font']
1151
for row, (label, cpath, vals, tip) in enumerate(info):
1152
itemcombo = widgets[row]
1153
itemcombo.child.set_text(preset[cpath])
1154
presetcombo.connect('changed', combo_changed)
1155
def radio_activated(radio, init=False):
1156
if not radio.get_active():
1158
presetcombo.set_sensitive(presetradio.get_active())
1159
table.set_sensitive(customradio.get_active())
1162
if radio == defaultradio:
1163
pagenum, info, vbox, widgets = self.pages['font']
1164
for row, (label, cpath, vals, tip) in enumerate(info):
1165
itemcombo = widgets[row]
1166
itemcombo.set_active(0) # unspecified
1167
if radio != presetradio:
1168
presetcombo.set_active(0)
1169
defaultradio.connect('toggled', radio_activated)
1170
presetradio.connect('toggled', radio_activated)
1171
customradio.connect('toggled', radio_activated)
1174
radio_activated(defaultradio, init=True)
1176
def refresh_font_frame(self):
1177
model = self.presetcombo.get_model()
1178
cpaths = [info[1] for info in self.pages['font'][1]]
1180
for cpath in cpaths:
1181
value = self.get_ini_config(cpath)
1182
if value is not None:
1185
# theme default fonts
1186
self.defaultradio.set_active(True)
1187
self.presetcombo.set_active(0)
1189
for name, (label, preset) in _font_presets.items():
1190
for cpath in cpaths:
1191
if self.get_ini_config(cpath) != preset[cpath]:
1195
rows = [row for row in model if row[0] == name]
1197
self.presetcombo.set_active_iter(rows[0].iter)
1198
self.presetradio.set_active(True)
1202
self.customradio.set_active(True)
1203
self.presetcombo.set_active(0)
1205
def fill_extensions_frame(self, parent, table):
1206
allexts = hglib.allextensions()
1209
maxrows = (len(allexts) + MAXCOLUMNS - 1) / MAXCOLUMNS
1210
extstable = gtk.Table()
1211
parent.pack_start(extstable, False, False)
1213
self.extensionschecks = {}
1214
for i, name in enumerate(sorted(allexts)):
1215
shortdesc = allexts[name]
1216
ck = gtk.CheckButton(name, use_underline=False)
1217
ck.connect('toggled', self.dirty_event)
1218
ck.connect('toggled', self._validateextensions)
1219
ck.connect('focus-in-event', self.set_help,
1220
hglib.toutf(shortdesc))
1221
col, row = i / maxrows, i % maxrows
1222
extstable.attach(ck, col, col + 1, row, row + 1)
1223
self.extensionschecks[name] = ck
1225
def _enabledextensions(self):
1226
"""list up to-be enabled extensions by reading fresh setting"""
1227
from mercurial import config
1228
cfg = config.config()
1229
for f in util.rcpath():
1230
if os.path.realpath(f) == os.path.realpath(self.fn):
1231
continue # can be modified by user; read from self.ini
1237
if 'extensions' in self.ini:
1238
for k in self.ini['extensions']:
1239
cfg.set('extensions', k, self.ini['extensions'][k])
1241
return set(name for name, path in cfg.items('extensions')
1242
if not path.startswith('!'))
1244
def refresh_extensions_frame(self):
1245
enabledexts = self._enabledextensions()
1246
for name, ck in self.extensionschecks.iteritems():
1247
ck.set_active(name in enabledexts)
1249
self._validateextensions()
1251
def _validateextensions(self, *args):
1252
enabledexts = self._enabledextensions()
1253
selectedexts = set(name for name, ck
1254
in self.extensionschecks.iteritems()
1256
invalidexts = hglib.validateextensions(selectedexts)
1258
def getinival(name):
1259
if 'extensions' not in self.ini:
1261
for cand in (name, 'hgext.%s' % name, 'hgext/%s' % name):
1263
v = self.ini['extensions'][cand]
1264
if not isinstance(v, Undefined):
1269
def changable(name):
1270
curval = getinival(name)
1271
if curval not in ('', None):
1272
# enabled or unspecified, official extensions only
1274
elif name in enabledexts and curval is None:
1275
# re-disabling ext is not supported
1277
elif name in invalidexts and name not in selectedexts:
1278
# disallow to enable bad exts, but allow to disable it
1283
allexts = hglib.allextensions()
1284
for name, ck in self.extensionschecks.iteritems():
1285
ck.set_sensitive(changable(name))
1286
self.tooltips.set_tip(ck, invalidexts.get(name)
1287
or hglib.toutf(allexts[name]))
1289
def apply_extensions_changes(self):
1290
enabledexts = self._enabledextensions()
1291
for name, ck in self.extensionschecks.iteritems():
1292
if ck.get_active() == (name in enabledexts):
1293
continue # unchanged
1295
if 'extensions' not in self.ini:
1296
new_namespace = getattr(self.ini, '_new_namespace',
1297
self.ini.new_namespace)
1298
new_namespace('extensions')
1301
self.ini['extensions'][name] = ''
1303
for cand in (name, 'hgext.%s' % name, 'hgext/%s' % name):
1305
del self.ini['extensions'][cand]
1309
def set_help(self, widget, event, tooltip):
1310
text = ' '.join(tooltip.splitlines())
1311
self.descbuffer.set_text(text)
1313
def fill_issue_frame(self, parent, table):
1314
parent.pack_start(table, False, False)
1316
issuetrackmodel = gtk.ListStore(str, # internal name
1318
issuetrackmodel.append((None, _(' - Select Issue Tracker -')))
1319
for (guid, label) in bugtraq.get_issue_plugins_with_names():
1320
issuetrackmodel.append((guid, label))
1321
issuetrackcombo = gtk.ComboBox(issuetrackmodel)
1322
cell = gtk.CellRendererText()
1323
issuetrackcombo.pack_start(cell)
1324
issuetrackcombo.add_attribute(cell, 'text', 1)
1325
self.issuetrackcombo = issuetrackcombo
1327
table.add_row('Issue plugin:', issuetrackcombo, padding=False)
1329
tooltip = _('Select issue tracker plugin to use. '
1330
'Links to plugins can be found at '
1331
'http://tortoisesvn.net/issuetrackerplugins')
1332
self.tooltips.set_tip(issuetrackcombo, tooltip)
1334
issuetrackcombo.connect('popup', self.set_help, '', tooltip)
1336
configurepluginbutton = gtk.Button(_('Configure Plugin'))
1337
table.add_row('', configurepluginbutton, padding=False)
1338
self.configurepluginbutton = configurepluginbutton
1340
def combo_changed(combo):
1341
model = combo.get_model()
1343
selection = model[self.issuetrackcombo.get_active()]
1344
if not selection[0] == None:
1345
newvalue = selection[0] + ' ' + selection[1]
1346
self.record_new_value("tortoisehg.issue.bugtraqplugin",
1349
self.configurepluginbutton.set_sensitive(newvalue != "")
1351
def configure_bugtraq_plugin(button):
1352
value = self.get_ini_config("tortoisehg.issue.bugtraqplugin")
1353
if value == None or value == "":
1356
guid = value.split(' ', 1)[0]
1357
value = self.get_ini_config("tortoisehg.issue.bugtraqparameters")
1361
bugtr = bugtraq.BugTraq(guid)
1362
newvalue = bugtr.show_options_dialog(value)
1364
if value != newvalue:
1365
self.record_new_value("tortoisehg.issue.bugtraqparameters", newvalue)
1368
issuetrackcombo.connect('changed', combo_changed)
1369
configurepluginbutton.connect('clicked', configure_bugtraq_plugin)
1371
def refresh_issue_frame(self):
1372
value = self.get_ini_config("tortoisehg.issue.bugtraqplugin")
1374
if not value == None:
1375
configuredguid = re.sub("[-{}]", "", value.split(' ', 1)[0])
1377
model = self.issuetrackcombo.get_model()
1380
for (guid, label) in model:
1381
if not guid == None:
1382
if re.sub("[-{}]", "", guid) == configuredguid:
1388
self.issuetrackcombo.set_active(index)
1389
self.configurepluginbutton.set_sensitive(configuredguid != "")
1391
def fill_frame(self, frame, info, build=True, width=32):
1394
table = gtklib.LayoutTable()
1398
vbox.pack_start(table, False, False)
1400
scrolled = gtk.ScrolledWindow()
1401
scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1402
scrolled.add_with_viewport(vbox)
1403
scrolled.child.set_shadow_type(gtk.SHADOW_NONE)
1404
frame.pack_start(scrolled)
1406
for row, (label, cpath, values, tooltip) in enumerate(info):
1407
vlist = gtk.ListStore(str, bool)
1408
combo = gtk.ComboBoxEntry(vlist, 0)
1409
combo.connect('changed', self.dirty_event)
1410
combo.child.connect('focus-in-event', self.set_help, tooltip)
1411
combo.set_row_separator_func(lambda model, path: model[path][1])
1412
combo.child.set_width_chars(width)
1413
if cpath in _pwfields:
1414
combo.child.set_visibility(False)
1415
widgets.append(combo)
1417
table.add_row(label + ':', combo, padding=False)
1418
self.tooltips.set_tip(combo, tooltip)
1420
return vbox, table, widgets
1422
def refresh_vlist(self, pagename=None):
1423
# sotre modification status
1424
prev_dirty = self.dirty
1426
# update configured values
1427
if pagename is None:
1428
pages = self.pages.values()
1429
pages = [(key,) + data for key, data in self.pages.items()]
1431
pages = ((pagename,) + self.pages[pagename],)
1432
for name, page_num, info, vbox, widgets in pages:
1435
for row, (label, cpath, values, tooltip) in enumerate(info):
1436
ispw = cpath in _pwfields
1437
combo = widgets[row]
1438
vlist = combo.get_model()
1441
# Get currently configured value from this config file
1442
curvalue = self.get_ini_config(cpath)
1444
if cpath == 'tortoisehg.vdiff':
1445
tools = hglib.difftools(self.ui)
1446
for key in tools.keys():
1447
if key not in values:
1449
elif cpath == 'ui.merge':
1450
# Special case, add [merge-tools] to possible values
1451
hglib.mergetools(self.ui, values)
1453
if cpath in _norepofields:
1454
combo.set_sensitive(
1455
self.confcombo.get_active() != CONF_REPO)
1459
vlist.append([_unspecstr, False])
1461
vlist.append([_('Suggested'), True])
1463
vlist.append([hglib.toutf(v), False])
1465
currow = len(vlist) - 1
1466
if cpath in self.history.get_keys() and not ispw:
1468
for v in self.history.mrul(cpath):
1469
if v in values: continue
1471
vlist.append([_('History'), True])
1473
vlist.append([hglib.toutf(v), False])
1475
currow = len(vlist) - 1
1477
if curvalue is None and len(vlist):
1479
elif currow is None and curvalue:
1480
combo.child.set_text(hglib.toutf(curvalue))
1482
combo.set_active(currow)
1485
func_name = 'refresh_%s_frame' % name
1486
if hasattr(self, func_name):
1487
getattr(self, func_name)()
1489
# clear modification status forcibly if need
1490
if self.dirty != prev_dirty:
1491
self._btn_apply.set_sensitive(False)
1494
def add_page(self, name):
1496
if name == data[0]['name']:
1501
extra, width = meta.get('extra', False), meta.get('width', 32)
1503
# setup frame and content
1507
vbox, table, widgets = self.fill_frame(frame, info, not extra, width)
1509
func = getattr(self, 'fill_%s_frame' % name, None)
1514
pagenum = self.notebook.append_page(frame)
1515
self.pages[name] = (pagenum, info, vbox, widgets)
1518
self.refresh_vlist(name)
1521
def get_ini_config(self, cpath):
1522
'''Retrieve a value from the parsed config file'''
1524
# Presumes single section/key level depth
1525
section, key = cpath.split('.', 1)
1526
if section not in self.ini:
1528
val = self.ini[section][key]
1529
if isinstance(val, Undefined):
1535
def load_config(self, rcpath):
1537
if os.path.exists(fn):
1541
# Try to create a file from rcpath
1544
f.write('# Generated by TortoiseHg setting dialog\n')
1547
except (IOError, OSError):
1550
gdialog.Prompt(_('Unable to create a Mercurial.ini file'),
1551
_('Insufficient access rights, reverting to read-only'
1552
'mode.'), self).run()
1553
from mercurial import config
1555
cfg = config.config()
1556
self.readonly = True
1561
# Monkypatch this regex to prevent iniparse from considering
1562
# 'rem' as a comment
1563
iniparse.ini.CommentLine.regex = \
1564
re.compile(r'^(?P<csep>[%;#])(?P<comment>.*)$')
1565
return iniparse.INIConfig(file(fn), optionxformvalue=None)
1567
from mercurial import config
1568
cfg = config.config()
1570
self.readonly = True
1572
except Exception, e:
1573
gdialog.Prompt(_('Unable to parse a config file'),
1574
_('%s\nReverting to read-only mode.') % str(e), self).run()
1575
from mercurial import config
1576
cfg = config.config()
1578
self.readonly = True
1581
def record_new_value(self, cpath, newvalue, keephistory=True):
1582
# 'newvalue' is converted to local encoding
1583
section, key = cpath.split('.', 1)
1584
if newvalue == _unspeclocalstr or newvalue == '':
1585
if section not in self.ini:
1588
del self.ini[section][key]
1592
if section not in self.ini:
1593
if hasattr(self.ini, '_new_namespace'):
1594
self.ini._new_namespace(section)
1596
self.ini.new_namespace(section)
1597
self.ini[section][key] = newvalue
1600
if cpath not in self.history.get_keys():
1601
self.history.set_value(cpath, [])
1602
elif newvalue in self.history.get_keys():
1603
self.history.get_value(cpath).remove(newvalue)
1604
self.history.mrul(cpath).add(newvalue)
1606
def apply_changes(self):
1608
#dialog? Read only access, please install ...
1610
# Reload history, since it may have been modified externally
1613
# flush changes on paths page
1614
if self.pages.has_key('sync'):
1615
if len(self.pathdata):
1617
for row in self.pathdata:
1618
name = hglib.fromutf(row[0])
1619
path = hglib.fromutf(row[2])
1621
gdialog.Prompt(_('Invalid path'),
1622
_('Skipped saving path with no alias'),
1625
cpath = '.'.join(['paths', name])
1626
self.record_new_value(cpath, path, False)
1627
refreshlist.append(name)
1628
for name in self.ini.paths:
1629
if name not in refreshlist:
1630
del self.ini['paths'][name]
1631
elif 'paths' in list(self.ini):
1632
for name in list(self.ini.paths):
1633
del self.ini['paths'][name]
1635
if 'extensions' in self.pages:
1636
self.apply_extensions_changes()
1638
# Flush changes on all pages
1639
for page_num, info, vbox, widgets in self.pages.values():
1640
for n, (label, cpath, values, tip) in enumerate(info):
1641
newvalue = hglib.fromutf(widgets[n].child.get_text())
1642
self.record_new_value(cpath, newvalue)
1644
self.history.write()
1645
self.refresh_vlist()
1648
f = util.atomictempfile(self.fn, 'w', createmode=None)
1649
f.write(str(self.ini))
1651
self._btn_apply.set_sensitive(False)
1654
dialog.error_dialog(self, _('Unable to write configuration file'),
1655
hglib.tounicode(str(e)))
1659
def run(ui, *pats, **opts):
1660
dlg = ConfigDialog(opts.get('alias') == 'repoconfig',
1661
focus=opts.get('focus'))