3 # Copyright (c) 2005 Fredrik Kuivinen <freku045@student.liu.se>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License version 2 as
7 # published by the Free Software Foundation.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 import sys
, math
, random
, qt
, os
, re
, signal
22 # Determine semantics according to executable name. Default to git.
23 if re
.split('/', sys
.argv
[0]).pop() == 'hgct':
26 print "defaulting to import git because arg = %s" % sys
.argv
[0]
29 qconnect
= qt
.QObject
.connect
31 applicationName
= 'Commit Tool'
36 class CommitError(Exception):
37 def __init__(self
, operation
, msg
):
38 self
.operation
= operation
44 class MyListItem(qt
.QCheckListItem
):
45 def __init__(self
, parent
, file, inSync
, commitMsg
= False):
49 status
= 'Working directory out of sync '
53 qt
.QListViewItem
.__init
__(self
, parent
, file.text
, qt
.QCheckListItem
.CheckBox
)
54 self
.setText(1, status
)
57 self
.commitMsg
= commitMsg
59 def compare(self
, item
, col
, asc
):
71 return cmp(self
.key(col
, asc
), item
.key(col
, asc
))
73 def paintCell(self
, p
, cg
, col
, w
, a
):
74 if col
== 1 and not self
.inSync
:
75 cg
.setColor(qt
.QColorGroup
.Text
, Qt
.red
)
76 qt
.QCheckListItem
.paintCell(self
, p
, cg
, col
, w
, a
)
77 cg
.setColor(qt
.QColorGroup
.Text
, Qt
.black
)
80 qt
.QListViewItem
.paintCell(self
, p
, cg
, col
, w
, a
)
82 qt
.QCheckListItem
.paintCell(self
, p
, cg
, col
, w
, a
)
85 return self
.state() == qt
.QCheckListItem
.On
87 def setSelected(self
, s
):
89 self
.setState(qt
.QCheckListItem
.On
)
91 self
.setState(qt
.QCheckListItem
.Off
)
93 class MyListView(qt
.QListView
):
94 def __init__(self
, parent
=None, name
=None):
95 qt
.QListView
.__init
__(self
, parent
, name
)
98 return ListViewIterator(self
)
100 class ListViewIterator
:
101 def __init__(self
, listview
):
102 self
.it
= qt
.QListViewItemIterator(listview
)
105 cur
= self
.it
.current()
113 raise StopIteration()
118 class MainWidget(qt
.QMainWindow
):
119 def __init__(self
, parent
=None, name
=None):
120 qt
.QMainWindow
.__init
__(self
, parent
, name
)
121 splitter
= qt
.QSplitter(Qt
.Vertical
, self
)
123 fW
= MyListView(splitter
)
125 fW
.setSelectionMode(qt
.QListView
.NoSelection
)
126 fW
.addColumn('Description')
127 statusTitle
= 'Cache Status'
128 fW
.addColumn(statusTitle
)
129 fW
.setResizeMode(qt
.QListView
.AllColumns
)
130 fW
.header().setStretchEnabled(1, False)
131 fW
.setColumnWidth(1, self
.fontMetrics().width(statusTitle
+ 'xxx'))
133 text
= qt
.QWidgetStack(splitter
)
135 self
.setCentralWidget(splitter
)
136 self
.setCaption(applicationName
)
138 self
.newCurLambda
= lambda i
: self
.currentChange(i
)
139 qconnect(fW
, qt
.SIGNAL("currentChanged(QListViewItem*)"), self
.newCurLambda
)
141 ops
= qt
.QPopupMenu(self
)
142 ops
.insertItem("Commit Selected Files", self
.commit
, Qt
.CTRL
+Qt
.Key_T
)
143 ops
.insertItem("Refresh", self
.refreshFiles
, Qt
.CTRL
+Qt
.Key_R
)
144 ops
.insertItem("Update Cache for Selected Files", self
.updateCacheSelected
, Qt
.CTRL
+Qt
.Key_U
)
147 m
.insertItem("&Operations", ops
)
149 h
= qt
.QPopupMenu(self
)
150 h
.insertItem("&About", self
.about
)
151 m
.insertItem("&Help", h
)
153 qconnect(fW
, qt
.SIGNAL("contextMenuRequested(QListViewItem*, const QPoint&, int)"),
154 self
.contextMenuRequestedSlot
)
155 self
.fileOps
= qt
.QPopupMenu(self
)
156 self
.fileOps
.insertItem("(Un)select", self
.toggleFile
)
157 self
.fileOps
.insertItem("Edit", self
.editFile
, Qt
.CTRL
+Qt
.Key_E
)
159 # The following attribute is set by contextMenuRequestedSlot and used
161 self
.currentContextItem
= None
163 self
.patchColors
= {'std': 'black', 'new': '#009600', 'remove': '#C80000', 'head': '#C800C8'}
167 self
.splitter
= splitter
171 f
.text
= "Commit message"
172 f
.textW
= self
.newTextEdit()
173 f
.textW
.setTextFormat(Qt
.PlainText
)
174 f
.textW
.setReadOnly(False)
176 self
.createCmitItem()
177 self
.splitter
.setSizes(eval(str(settings
.readEntry('splitter', '[400, 200]')[0])))
179 def closeEvent(self
, e
):
181 settings
.writeEntry('x', p
.x()),
182 settings
.writeEntry('y', p
.y()),
185 settings
.writeEntry('width', s
.width()),
186 settings
.writeEntry('height', s
.height())
188 settings
.writeEntry('splitter', str(self
.splitter
.sizes()))
191 def createCmitItem(self
):
192 self
.cmitItem
= MyListItem(self
.filesW
, self
.cmitFile
, False, True)
193 self
.cmitItem
.setSelectable(False)
194 self
.filesW
.insertItem(self
.cmitItem
)
196 def about(self
, ignore
):
197 qt
.QMessageBox
.about(self
, "About " + applicationName
,
198 "<qt><center><h1>" + applicationName
+ " " + version
+ """</h1></center>\n
199 <center>Copyright © 2005 Fredrik Kuivinen <freku045@student.liu.se>
200 </center>\n<p>This program is free software; you can redistribute it and/or
201 modify it under the terms of the GNU General Public License version 2 as
202 published by the Free Software Foundation.</p></qt>""")
204 def contextMenuRequestedSlot(self
, item
, pos
, col
):
205 if item
and not item
.commitMsg
:
206 self
.currentContextItem
= item
207 self
.fileOps
.exec_loop(qt
.QCursor
.pos())
209 self
.currentContextItem
= None
211 def toggleFile(self
, ignored
):
212 it
= self
.currentContextItem
217 it
.setSelected(False)
221 def editFile(self
, ignored
):
222 it
= self
.currentContextItem
228 qt
.QMessageBox
.warning(self
, 'No editor found',
229 '''No editor found. Gct looks for an editor to execute in the environment
230 variable GCT_EDITOR, if that variable is not set it will use the variable
233 # We can't use runProgram([ed, it.file.dstName]) here because
234 # ed might be something like 'xemacs -nw' which has to be
235 # interpreted by the shell.
237 runProgram(ed
+ ' ' + shellQuote(it
.file.dstName
))
238 doUpdateCache(it
.file.dstName
)
240 except ProgramError
, e
:
241 qt
.QMessageBox
.warning(self
, 'Failed to launch editor',
242 '''Gct failed to launch the editor. The command used was: ''' + e
.program
)
244 def currentChange(self
, item
):
245 self
.text
.raiseWidget(item
.file.textW
)
248 def updateCacheSelected(self
, id):
249 for it
in self
.selectedItems():
250 scm
.doUpdateCache(it
.file.dstName
)
253 def selectedItems(self
):
255 for item
in self
.filesW
:
256 if item
.isSelected():
260 def commit(self
, id):
265 for item
in self
.filesW
:
266 debug("file: " + item
.file.text
)
267 if item
.isSelected():
268 selFileNames
.append(item
.file.text
)
269 commitFiles
.append(item
.file.dstName
)
271 keepFiles
.append(item
.file)
273 commitMsg
= str(self
.cmitItem
.file.textW
.text())
276 qt
.QMessageBox
.information(self
, "Commit - " + applicationName
,
277 "No files selected for commit.", "&Ok")
280 commitMsg
= fixCommitMsgWhiteSpace(commitMsg
)
281 if(qt
.QMessageBox
.question(self
, "Confirm Commit - " + applicationName
,
282 '''<qt><p>Do you want to commit the following file(s):</p><blockquote>''' +
283 '<br>'.join(selFileNames
) +
284 '''</blockquote><p>with the commit message:</p><blockquote><pre>''' +
285 str(qt
.QStyleSheet
.escape(commitMsg
)) + '</pre></blockquote></qt>',
290 scm
.doCommit(keepFiles
, commitFiles
, commitMsg
)
291 except CommitError
, e
:
292 qt
.QMessageBox
.warning(self
, "Commit Failed - " + applicationName
,
293 "Commit failed during " + e
.operation
+ ": " + e
.msg
,
296 qt
.QMessageBox
.warning(self
, "Commit Failed - " + applicationName
,
297 "Commit failed: " + e
.strerror
,
300 self
.cmitItem
.file.textW
.setText('')
303 def getFileState(self
):
305 cur
= self
.filesW
.currentItem()
306 if cur
and cur
!= self
.cmitItem
:
307 ret
.current
= self
.filesW
.currentItem().file.srcName
312 for x
in self
.filesW
:
314 ret
.selected
[x
.file.srcName
] = True
317 def restoreFileState(self
, state
):
318 for x
in self
.filesW
:
319 if state
.selected
.has_key(x
.file.srcName
):
321 if x
.file.srcName
== state
.current
:
322 self
.filesW
.setCurrentItem(x
)
324 def newTextEdit(self
):
326 self
.text
.addWidget(ret
)
329 def setFiles(self
, files
):
330 state
= self
.getFileState()
332 self
.createCmitItem()
334 self
.text
.removeWidget(f
.textW
)
338 f
.textW
= self
.newTextEdit()
339 f
.textW
.setReadOnly(False)
340 f
.textW
.setTextFormat(Qt
.RichText
)
341 f
.textW
.setText(formatPatchRichText(f
.patch
, self
.patchColors
))
343 self
.filesW
.insertItem(MyListItem(self
.filesW
, f
, f
.updated
))
345 self
.filesW
.setCurrentItem(self
.cmitItem
)
347 # For some reason the currentChanged signal isn't emitted
348 # here. We call currentChange ourselves instead.
349 self
.currentChange(self
.cmitItem
)
351 self
.restoreFileState(state
)
353 def refreshFiles(self
, ignored
=None):
355 self
.setFiles(scm
.getFiles())
357 commitMsgRE
= re
.compile('[ \t\r\f\v]*\n\\s*\n')
358 def fixCommitMsgWhiteSpace(msg
):
361 msg
= re
.sub(commitMsgRE
, '\n', msg
)
365 def formatPatchRichText(patch
, colors
):
366 ret
= ['<qt><pre><font color="', colors
['std'], '">']
368 for l
in patch
.split('\n'):
375 if c
== '+': style
= 'new'
376 elif c
== '-': style
= 'remove'
377 elif c
== '@': style
= 'head'
379 ret
.extend(['</font><font color="', colors
[style
], '">'])
381 ret
.extend([str(qt
.QStyleSheet
.escape(l
)), '\n'])
382 ret
.append('</pre></qt>')
385 class ProgramError(Exception):
386 def __init__(self
, program
, err
):
387 self
.program
= program
391 if os
.environ
.has_key('GCT_EDITOR'):
392 return os
.environ
['GCT_EDITOR']
393 elif os
.environ
.has_key('EDITOR'):
394 return os
.environ
['EDITOR']
407 return "'" + res
+ "'"
409 def runProgram(prog
, input=None):
410 debug('runProgram prog: ' + str(prog
) + " input: " + str(input))
411 if type(prog
) is str:
414 progStr
= ' '.join(prog
)
417 pop
= subprocess
.Popen(prog
,
418 shell
= type(prog
) is str,
419 stderr
=subprocess
.STDOUT
,
420 stdout
=subprocess
.PIPE
,
421 stdin
=subprocess
.PIPE
)
423 debug("strerror: " + e
.strerror
)
424 raise ProgramError(progStr
, e
.strerror
)
427 pop
.stdin
.write(input)
430 out
= pop
.stdout
.read()
433 debug("error output: " + out
)
434 raise ProgramError(progStr
, out
)
435 debug("output: " + out
.replace('\0', '\n'))
440 app
= qt
.QApplication(sys
.argv
)
441 settings
= qt
.QSettings()
442 settings
.beginGroup('/' + shortName
)
443 settings
.beginGroup('/geometry/')
448 mw
.resize(settings
.readNumEntry('width', 500)[0],
449 settings
.readNumEntry('height', 600)[0])
452 # The following code doesn't work correctly in some (at least
454 # managers. http://doc.trolltech.com/3.3/geometry.html contains some
455 # information about this issue.
456 # mw.move(settings.readNumEntry('x', 100)[0],
457 # settings.readNumEntry('y', 100)[0])
460 app
.setMainWidget(mw
)
463 # Handle CTRL-C appropriately
464 signal
.signal(signal
.SIGINT
, lambda s
, f
: app
.quit())
466 ret
= app
.exec_loop()