Kiln » TortoiseHg » TortoiseHg
Clone URL:  
Pushed to one repository · View In Graph Contained in 1.9, 1.9.1, and 1.9.2

hgqt: add cmdui.py and thread.py

Foundamental modules to execute Mercurial commands.

Changeset 0686d6774b76

Parent 1786f41873f1

by Yuki KODAMA

Changes to 5 files · Browse files at 0686d6774b76 Showing diff from parent 1786f41873f1 Diff from another changeset...

Change 1 of 1 Show Entire File tortoisehg/​hgqt/​__init__.py Stacked
 
 
 
1
@@ -0,0 +1,1 @@
+#placeholder
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​cmdui.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@@ -0,0 +1,165 @@
+# cmdui.py - A widget to execute Mercurial command for TortoiseHg +# +# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2, incorporated herein by reference. + +from PyQt4.QtCore import Qt, QString +from PyQt4.QtGui import QDialog, QDialogButtonBox, QLabel, QProgressBar +from PyQt4.QtGui import QTextEdit, QHBoxLayout, QGridLayout, QMessageBox + +from tortoisehg.hgqt.i18n import _, localgettext +from tortoisehg.hgqt import qtlib, thread + +local = localgettext() + +class Dialog(QDialog): + def __init__(self, cmdline, parent=None): + super(Dialog, self).__init__(parent, Qt.WindowTitleHint or \ + Qt.WindowSystemMenuHint) + + # main layout grid + grid = QGridLayout() + grid.setSpacing(6) + grid.setContentsMargins(*(7,)*4) + + ## hbox for status and progress labels + hbox = QHBoxLayout() + grid.addLayout(hbox, 0, 0) + + self.status_label = QLabel('') + hbox.addWidget(self.status_label, 1) + + self.prog_label = QLabel('') + hbox.addWidget(self.prog_label, 0, Qt.AlignRight) + + self.pbar = QProgressBar() + self.pbar.setTextVisible(False) + self.pbar.setMinimum(0) + grid.addWidget(self.pbar, 1, 0) + + # command output area + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + grid.addWidget(self.log_text, 2, 0, 5, 0) + grid.setRowStretch(2, 1) + + # bottom buttons + buttons = QDialogButtonBox() + self.cancel_btn = buttons.addButton(QDialogButtonBox.Cancel) + self.cancel_btn.clicked.connect(self.cancel_clicked) + self.close_btn = buttons.addButton(QDialogButtonBox.Close) + self.close_btn.setHidden(True) + self.close_btn.clicked.connect(self.reject) + grid.addWidget(buttons, 7, 0) + + self.setLayout(grid) + self.setWindowTitle(_('TortoiseHg Command Dialog')) + self.resize(540, 420) + + # setup and start command thread + self.cmd = thread.CmdThread(cmdline) + self.cmd.outputReceived.connect(self.output_received) + self.cmd.errorReceived.connect(self.error_received) + self.cmd.progressReceived.connect(self.progress_received) + self.cmd.started.connect(self.command_started) + self.cmd.commandFinished.connect(self.command_finished) + self.cmd.start() + + def reject(self): + if self.cmd.isRunning(): + ret = QMessageBox.question(self, _('Confirm Exit'), _('Mercurial' + ' command is still running.\nAre you sure you want' + ' to terminate?'), QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + if ret == QMessageBox.Yes: + self.cancel_clicked() + + # don't close dialog + return + + # close dialog + QDialog.reject(self) + + def cancel_clicked(self): + # trigger KeyboardInterrupt + self.cmd.abort() + + # until thread is terminated + self.cancel_btn.setDisabled(True) + self.pbar.setMaximum(0) + + def command_started(self): + # use indeterminate mode + self.pbar.setMaximum(0) + self.status_label.setText(_('Running...')) + + def command_finished(self, wrapper): + ret = wrapper.data + if ret is None or self.pbar.maximum() == 0: + self.clear_progress() + if ret is None: + if self.cmd.abortbyuser: + status = _('Terminated by user') + else: + status = _('Terminated') + else: + status = _('Finished') + self.status_label.setText(status) + self.cancel_btn.setHidden(True) + self.close_btn.setShown(True) + self.close_btn.setFocus() + + def append_output(self, msg, style=''): + if isinstance(msg, str): + msg = unicode(msg, 'mbcs') + msg = msg.replace('\n', '<br />') + self.log_text.insertHtml('<pre style="%s">%s</pre>' % (style, msg)) + + def output_received(self, wrapper): + msg, label = wrapper.data + style = qtlib.LabelStyles.get(label, '') + style += 'font-size: 9pt;' + self.append_output(msg, style) + + def error_received(self, wrapper): + self.append_output(wrapper.data, qtlib.LabelStyles['error']) + + def clear_progress(self): + self.pbar.reset() + self.pbar.setMaximum(100) + self.status_label.setText('') + self.prog_label.setText('') + self.inprogress = False + + def progress_received(self, wrapper): + if self.cmd.isFinished(): + self.clear_progress() + return + + counting = False + topic, item, pos, total, unit = wrapper.data + if pos is None: + self.clear_progress() + return + if total is None: + count = '%d' % pos + counting = True + else: + self.pbar.setMaximum(total) + self.pbar.setValue(pos) + count = '%d / %d' % (pos, total) + if unit: + count += ' ' + unit + self.prog_label.setText(unicode(count, 'mbcs')) + if item: + status = '%s: %s' % (topic, item) + else: + status = local._('Status: %s') % topic + self.status_label.setText(unicode(status, 'mbcs')) + self.inprogress = True + + if not self.inprogress or counting: + # use indeterminate mode + self.pbar.setMinimum(0)
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​i18n.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -0,0 +1,16 @@
+# i18n.py - internationalization support for TortoiseHg +# +# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2, incorporated herein by reference. + +from tortoisehg.util.i18n import _ as _gettext +from tortoisehg.util.i18n import agettext, keepgettext + +def _(message): + return unicode(_gettext(message), 'utf-8') + +class localgettext(object): + def _(self, message): + return agettext(message)
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​qtlib.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@@ -0,0 +1,88 @@
+# common colors + +DRED = '#900000' +DGREEN = '#006400' +DBLUE = '#000090' +DYELLOW = '#6A6A00' +DORANGE = '#AA5000' +DGRAY = '#404040' + +PRED = '#ffcccc' +PGREEN = '#aaffaa' +PBLUE = '#aaddff' +PYELLOW = '#ffffaa' +PORANGE = '#ffddaa' + +RED = 'red' +GREEN = 'green' +BLUE = 'blue' +YELLOW = 'yellow' +BLACK = 'black' +WHITE = 'white' +GRAY = 'gray' + +NORMAL = BLACK +NEW_REV_COLOR = DGREEN +CHANGE_HEADER = GRAY +UP_ARROW_COLOR = '#feaf3e' +DOWN_ARROW_COLOR = '#8ae234' +STAR_COLOR = '#fce94f' +CELL_GRAY = '#2e3436' +STATUS_HEADER = '#DDDDDD' +STATUS_REJECT_BACKGROUND = '#EEEEEE' +STATUS_REJECT_FOREGROUND = '#888888' + +LabelStyles = { + 'error': 'font-weight: bold; color: %s;' % DRED, + 'control': 'font-weight: bold; color: %s;' % BLACK, + 'ui.debug': 'font-weight: lighter; color: %s;' % BLACK, + 'ui.status': 'color: %s;' % DGRAY, + 'ui.note': 'color: %s;' % BLACK, + 'ui.warning': 'font-weight: bold; color: %s;' % RED, + 'log.summary': 'color: %s;' % BLACK, + 'log.description': 'color: %s;' % DGRAY, + 'log.changeset': 'color: %s;' % GRAY, + 'log.tag': 'color: %s;' % RED, + 'log.user': 'color: %s;' % BLUE, + 'log.date': 'color: %s;' % BLACK, + 'log.files': 'color: %s;' % BLACK, + 'log.copies': 'font-weight: bold; color: %s;' % BLACK, + 'log.node': 'color: %s;' % BLACK, + 'log.branch': 'color: %s;' % BLACK, + 'log.parent': 'color: %s;' % BLACK, + 'log.manifest': 'color: %s;' % BLACK, + 'log.extra': 'color: %s;' % BLACK, + 'diff.diffline': 'color: %s;' % BLACK, + 'diff.inserted': 'color: %s;' % DGREEN, + 'diff.deleted': 'color: %s;' % RED, + 'diff.hunk': 'color: %s;' % BLUE, + 'diff.file_a': 'font-weight: bold; color: %s;' % BLACK, + 'diff.file_b': 'font-weight: bold; color: %s;' % BLACK, +} + +# These labels are unreachable by TortoiseHg consoles, so we leave them +# out for efficiency +unusedLabelStyles = { + 'qseries.applied': 'color: %s;' % BLACK, + 'qseries.unapplied':'color: %s;' % DGRAY, + 'qseries.guarded': 'color: %s;' % BLUE, + 'qseries.missing': 'color: %s;' % DRED, + 'qguard.patch': 'color: %s;' % BLACK, + 'qguard.positive': 'color: %s;' % DGREEN, + 'qguard.negagive': 'color: %s;' % BLUE, + 'qguard.unguarded': 'color: %s;' % DGRAY, + 'diffstat.inserted':'color: %s;' % DGREEN, + 'diffstat.deleted': 'color: %s;' % RED, + 'bookmarks.current':'font-weight: bold; color: %s;' % BLACK, + 'resolve.resolved': 'color: %s;' % DGREEN, + 'resolve.unresolved':'color: %s;' % RED, + 'grep.match': 'font-weight: bold; color: %s;' % BLACK, + 'status.modified': 'color: %s;' % BLACK, + 'status.added': 'color: %s;' % BLACK, + 'status.removed': 'color: %s;' % BLACK, + 'status.missing': 'color: %s;' % BLACK, + 'status.unknown': 'color: %s;' % BLACK, + 'status.ignored': 'color: %s;' % BLACK, + 'status.clean': 'color: %s;' % BLACK, + 'status.copied': 'color: %s;' % BLACK, +}
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​thread.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
@@ -0,0 +1,226 @@
+# thread.py - A seprated thread to run Mercurial command +# +# Copyright 2009 Steve Borho <steve@borho.org> +# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2, incorporated herein by reference. + +import Queue +import time +import urllib2 + +from PyQt4.QtCore import SIGNAL, pyqtSignal, QObject, QThread +from PyQt4.QtGui import QMessageBox, QInputDialog, QLineEdit + +from mercurial import ui, util, error, dispatch + +from tortoisehg.util import thread2, hglib +from tortoisehg.hgqt.i18n import _, localgettext + +local = localgettext() + +SIG_OUTPUT = SIGNAL('output(PyQt_PyObject)') +SIG_ERROR = SIGNAL('error(PyQt_PyObject)') +SIG_INTERACT = SIGNAL('interact(PyQt_PyObject)') +SIG_PROGRESS = SIGNAL('progress(PyQt_PyObject)') + +class QtUi(ui.ui): + def __init__(self, src=None, responseq=None): + super(QtUi, self).__init__(src) + + if src: + self.sig = src.sig + self.responseq = src.responseq + else: + self.sig = QObject() # dummy object to emit signals + self.responseq = responseq + + self.setconfig('ui', 'interactive', 'on') + self.setconfig('progress', 'disable', 'True') + + def write(self, *args, **opts): + if self._buffers: + self._buffers[-1].extend([str(a) for a in args]) + else: + for a in args: + data = DataWrapper((str(a), opts.get('label', ''))) + self.sig.emit(SIG_OUTPUT, data) + + def write_err(self, *args, **opts): + for a in args: + data = DataWrapper(str(a)) + self.sig.emit(SIG_ERROR, data) + + def label(self, msg, label): + return msg + + def flush(self): + pass + + def prompt(self, msg, choices=None, default='y'): + if not self.interactive(): return default + try: + # emit SIG_INTERACT signal + data = DataWrapper((msg, False, choices, None)) + self.sig.emit(SIG_INTERACT, data) + # await response + r = self.responseq.get(True) + if r is None: + raise EOFError + if not r: + return default + if choices: + # return char for Mercurial 1.3 + choice = choices[r] + return choice[choice.index('&')+1].lower() + return r + except EOFError: + raise util.Abort(local._('response expected')) + + def promptchoice(self, msg, choices, default=0): + if not self.interactive(): return default + try: + # emit SIG_INTERACT signal + data = DataWrapper((msg, False, choices, default)) + self.sig.emit(SIG_INTERACT, data) + # await response + r = self.responseq.get(True) + if r is None: + raise EOFError + return r + except EOFError: + raise util.Abort(local._('response expected')) + + def getpass(self, prompt=_('password: '), default=None): + # emit SIG_INTERACT signal + data = DataWrapper((prompt, True, None, default)) + self.sig.emit(SIG_INTERACT, data) + # await response + r = self.responseq.get(True) + if r is None: + raise util.Abort(local._('response expected')) + return r + + def progress(self, topic, pos, item='', unit='', total=None): + data = DataWrapper((topic, item, pos, total, unit)) + self.sig.emit(SIG_PROGRESS, data) + +class DataWrapper(QObject): + def __init__(self, data): + super(DataWrapper, self).__init__(None) + self.data = data + +class CmdThread(QThread): + """Run an Mercurial command in a background thread, implies output + is being sent to a rendered text buffer interactively and requests + for feedback from Mercurial can be handled by the user via dialog + windows. + """ + # (msg=str, label=str) [wrapped] + outputReceived = pyqtSignal(DataWrapper) + + # msg=str [wrapped] + errorReceived = pyqtSignal(DataWrapper) + + # (msg=str, password=bool, choices=tuple, default=str) [wrapped] + # password: whether should be masked by asterisk chars + # choices: tuple of choice strings + interactReceived = pyqtSignal(DataWrapper) + + # (topic=str, item=str, pos=int, total=int, unit=str) [wrapped] + progressReceived = pyqtSignal(DataWrapper) + + # result=int or None [wrapped] + # result: None - command is incomplete, possibly exited with exception + # 0 - command is finished successfully + # others - return code of command + commandFinished = pyqtSignal(DataWrapper) + + def __init__(self, cmdline, parent=None): + super(QThread, self).__init__(None) + + self.cmdline = cmdline + self.parent = parent + self.ret = None + self.abortbyuser = False + self.responseq = Queue.Queue() + self.ui = QtUi(responseq=self.responseq) + + # Re-emit all ui.sig's signals to CmdThread (self). + # QSignalMapper doesn't help for this since our SIGNAL + # parameters contain 'PyQt_PyObject' types. + for name, sig in ((SIG_OUTPUT, self.outputReceived), + (SIG_ERROR, self.errorReceived), + (SIG_INTERACT, self.interactReceived), + (SIG_PROGRESS, self.progressReceived)): + def repeater(sig): # hide 'sig' local variable name + return lambda data: sig.emit(data) + self.connect(self.ui.sig, name, repeater(sig)) + + self.finished.connect(self.thread_finished) + self.interactReceived.connect(self.interact_handler) + + def abort(self): + if self.isRunning() and hasattr(self, 'thread_id'): + self.abortbyuser = True + thread2._async_raise(self.thread_id, KeyboardInterrupt) + + def thread_finished(self): + self.commandFinished.emit(DataWrapper(self.ret)) + + def interact_handler(self, wrapper): + prompt, password, choices, default = wrapper.data + if isinstance(prompt, str): + prompt = unicode(prompt, 'mbcs') + if choices: + dlg = QMessageBox(QMessageBox.Question, + _('TortoiseHg Prompt'), prompt, + QMessageBox.Yes | QMessageBox.Cancel, self.parent) + dlg.setDefaultButton(QMessageBox.Cancel) + rmap = {} + for index, choice in enumerate(choices): + button = dlg.addButton(unicode(choice, 'mbcs'), + QMessageBox.ActionRole) + rmap[id(button)] = index + dlg.exec_() + button = dlg.clickedButton() + if button is 0: + result = default + else: + result = rmap[id(button)] + self.responseq.put(result) + else: + mode = password and QLineEdit.Password \ + or QLineEdit.Normal + text, ok = QInputDialog().getText(self.parent, + _('TortoiseHg Prompt'), prompt, mode) + self.responseq.put(ok and text or None) + + def run(self): + # save thread id in order to terminate by KeyboardInterrupt + self.thread_id = int(QThread.currentThreadId()) + + try: + for k, v in self.ui.configitems('defaults'): + self.ui.setconfig('defaults', k, '') + self.ret = dispatch._dispatch(self.ui, self.cmdline) or 0 + except util.Abort, e: + self.ui.write_err(local._('abort: ') + str(e) + '\n') + except (error.RepoError, urllib2.HTTPError), e: + self.ui.write_err(str(e) + '\n') + except (Exception, OSError, IOError), e: + self.ui.write_err(str(e) + '\n') + except KeyboardInterrupt: + pass + + if self.ret is None: + if self.abortbyuser: + msg = _('[command terminated by user %s]') + else: + msg = _('[command interrupted %s]') + elif self.ret: + msg = _('[command returned code %d %%s]') % int(ret) + else: + msg = _('[command completed successfully %s]') + self.ui.write(msg % time.asctime() + '\n', label='control')