settings.py is a module now, make sure we "compile" it.
[hgct.git] / main.py
blob0f9c664f06c92a2a9c308e70a28d36f4e5f128bb
1 #!/usr/bin/env python
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.
8 #
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, sets, settings
20 from ctcore import *
22 # Determine semantics according to executable name. Default to git.
23 if os.path.basename(sys.argv[0]) == 'hgct':
24 print "defaulting to import hg because arg = %s" % sys.argv[0]
25 import hg as scm
26 else:
27 import git as scm
29 qconnect = qt.QObject.connect
30 Qt = qt.Qt
31 #DEBUG = 1
33 class CommitError(Exception):
34 def __init__(self, operation, msg):
35 self.operation = operation
36 self.msg = msg
38 class FileState:
39 pass
41 class MyListItem(qt.QCheckListItem):
42 def __init__(self, parent, file, commitMsg = False):
43 qt.QCheckListItem.__init__(self, parent, file.text, qt.QCheckListItem.CheckBox)
44 self.file = file
45 self.commitMsg = commitMsg
47 def compare(self, item, col, asc):
48 if self.commitMsg:
49 if asc:
50 return -1
51 else:
52 return 1
53 elif item.commitMsg:
54 if asc:
55 return 1
56 else:
57 return -1
58 else:
59 return cmp(self.key(col, asc), item.key(col, asc))
61 def paintCell(self, p, cg, col, w, a):
62 if self.commitMsg:
63 qt.QListViewItem.paintCell(self, p, cg, col, w, a)
64 else:
65 qt.QCheckListItem.paintCell(self, p, cg, col, w, a)
67 def isSelected(self):
68 return self.state() == qt.QCheckListItem.On
70 def setSelected(self, s):
71 if s:
72 self.setState(qt.QCheckListItem.On)
73 else:
74 self.setState(qt.QCheckListItem.Off)
76 class MyListView(qt.QListView):
77 def __init__(self, parent=None, name=None):
78 qt.QListView.__init__(self, parent, name)
80 def __iter__(self):
81 return ListViewIterator(self)
83 class ListViewIterator:
84 def __init__(self, listview):
85 self.it = qt.QListViewItemIterator(listview)
87 def next(self):
88 cur = self.it.current()
89 if cur:
90 self.it += 1
91 if cur.commitMsg:
92 return self.next()
93 else:
94 return cur
95 else:
96 raise StopIteration()
98 def __iter__(self):
99 return self
101 class MainWidget(qt.QMainWindow):
102 def __init__(self, parent=None, name=None):
103 qt.QMainWindow.__init__(self, parent, name)
104 splitter = qt.QSplitter(Qt.Vertical, self)
106 fW = MyListView(splitter)
107 fW.setFocus()
108 fW.setSelectionMode(qt.QListView.NoSelection)
109 fW.addColumn('Description')
110 fW.setResizeMode(qt.QListView.AllColumns)
112 text = qt.QWidgetStack(splitter)
114 self.setCentralWidget(splitter)
115 self.setCaption(applicationName)
117 self.newCurLambda = lambda i: self.currentChange(i)
118 qconnect(fW, qt.SIGNAL("currentChanged(QListViewItem*)"), self.newCurLambda)
120 ops = qt.QPopupMenu(self)
121 ops.insertItem("Commit Selected Files", self.commit, Qt.CTRL+Qt.Key_T)
122 ops.insertItem("Refresh", self.refreshFiles, Qt.CTRL+Qt.Key_R)
123 ops.insertItem("Select All", self.selectAll, Qt.CTRL+Qt.Key_A)
124 ops.insertItem("Unselect All", self.unselectAll, Qt.CTRL+Qt.Key_U)
125 ops.insertItem("Preferences...", self.showPrefs, Qt.CTRL+Qt.Key_P)
127 m = self.menuBar()
128 m.insertItem("&Operations", ops)
130 h = qt.QPopupMenu(self)
131 h.insertItem("&About", self.about)
132 m.insertItem("&Help", h)
134 qconnect(fW, qt.SIGNAL("contextMenuRequested(QListViewItem*, const QPoint&, int)"),
135 self.contextMenuRequestedSlot)
136 self.fileOps = qt.QPopupMenu(self)
137 self.fileOps.insertItem("Toggle selection", self.toggleFile)
138 self.fileOps.insertItem("Edit", self.editFile, Qt.CTRL+Qt.Key_E)
139 self.fileOps.insertItem("Discard changes", self.discardFile)
140 self.fileOps.insertItem("Ignore file", self.ignoreFile)
142 # The following attribute is set by contextMenuRequestedSlot
143 # and currentChange and used by the fileOps
144 self.currentContextItem = None
146 self.patchColors = {'std': 'black', 'new': '#009600', 'remove': '#C80000', 'head': '#C800C8'}
148 self.filesW = fW
149 self.files = []
150 self.splitter = splitter
151 self.text = text
153 f = File()
154 f.text = "Commit message"
155 f.textW = self.newTextEdit()
156 f.textW.setTextFormat(Qt.PlainText)
157 f.textW.setReadOnly(False)
158 f.textW.setText(settings.signoff)
160 self.cmitFile = f
161 self.createCmitItem()
162 self.editorProcesses = sets.Set()
163 self.loadSettings()
165 def loadSettings(self):
166 self.splitter.setSizes(settings.splitter)
168 def closeEvent(self, e):
169 s = self.size()
170 settings.width = s.width()
171 settings.height = s.height()
172 settings.splitter = self.splitter.sizes()
173 e.accept()
175 def createCmitItem(self):
176 self.cmitItem = MyListItem(self.filesW, self.cmitFile, True)
177 self.cmitItem.setSelectable(False)
178 self.filesW.insertItem(self.cmitItem)
180 def about(self, ignore):
181 qt.QMessageBox.about(self, "About " + applicationName,
182 "<qt><center><h1>" + applicationName + " " + version + """</h1></center>\n
183 <center>Copyright &copy; 2005 Fredrik Kuivinen &lt;freku045@student.liu.se&gt;
184 </center>\n<p>This program is free software; you can redistribute it and/or
185 modify it under the terms of the GNU General Public License version 2 as
186 published by the Free Software Foundation.</p></qt>""")
188 def contextMenuRequestedSlot(self, item, pos, col):
189 if item and not item.commitMsg:
190 self.currentContextItem = item
191 self.fileOps.exec_loop(qt.QCursor.pos())
192 else:
193 self.currentContextItem = None
195 def toggleFile(self, ignored):
196 it = self.currentContextItem
197 if not it:
198 return
200 if it.isSelected():
201 it.setSelected(False)
202 else:
203 it.setSelected(True)
205 def editFile(self, ignored):
206 it = self.currentContextItem
207 if not it:
208 return
210 ed = getEditor()
211 if not ed:
212 qt.QMessageBox.warning(self, 'No editor found',
213 '''No editor found. Gct looks for an editor to execute in the environment
214 variable GCT_EDITOR, if that variable is not set it will use the variable
215 EDITOR.''')
216 return
218 # This piece of code is not entirely satisfactory. If the user
219 # has EDITOR set to 'vi', or some other non-X application, the
220 # editor will be started in the terminal which (h)gct was
221 # started in. A better approach would be to close stdin and
222 # stdout after the fork but before the exec, but this doesn't
223 # seem to be possible with QProcess.
224 p = qt.QProcess(ed)
225 p.addArgument(it.file.dstName)
226 p.setCommunication(0)
227 qconnect(p, qt.SIGNAL('processExited()'), self.editorExited)
228 if not p.launch(qt.QByteArray()):
229 qt.QMessageBox.warning(self, 'Failed to launch editor',
230 shortName + ' failed to launch the ' + \
231 'editor. The command used was: ' + \
232 ed + ' ' + it.file.dstName)
233 else:
234 self.editorProcesses.add(p)
236 def editorExited(self):
237 p = self.sender()
238 status = p.exitStatus()
239 file = str(p.arguments()[1])
240 editor = str(p.arguments()[0]) + ' ' + file
241 if not p.normalExit():
242 qt.QMessageBox.warning(self, 'Editor failure',
243 'The editor, ' + editor + ', exited abnormally.')
244 elif status != 0:
245 qt.QMessageBox.warning(self, 'Editor failure',
246 'The editor, ' + editor + ', exited with exit code ' + str(status))
248 self.editorProcesses.remove(p)
249 scm.doUpdateCache(file)
250 self.refreshFiles()
252 def discardFile(self, ignored):
253 it = self.currentContextItem
254 if not it:
255 return
257 scm.discardFile(it.file)
258 self.refreshFiles()
260 def ignoreFile(self, ignored):
261 it = self.currentContextItem
262 if not it:
263 return
265 scm.ignoreFile(it.file)
266 self.refreshFiles()
268 def currentChange(self, item):
269 self.text.raiseWidget(item.file.textW)
270 self.text.update()
271 self.currentContextItem = item
273 def selectedItems(self):
274 ret = []
275 for item in self.filesW:
276 if item.isSelected():
277 ret.append(item)
278 return ret
280 def commit(self, id):
281 selFileNames = []
282 keepFiles = []
283 commitFiles = []
285 for item in self.filesW:
286 debug("file: " + item.file.text)
287 if item.isSelected():
288 selFileNames.append(item.file.text)
289 commitFiles.append(item.file.dstName)
290 else:
291 keepFiles.append(item.file)
293 commitMsg = str(self.cmitItem.file.textW.text())
295 if not selFileNames:
296 qt.QMessageBox.information(self, "Commit - " + applicationName,
297 "No files selected for commit.", "&Ok")
298 return
300 commitMsg = fixCommitMsgWhiteSpace(commitMsg)
301 if(qt.QMessageBox.question(self, "Confirm Commit - " + applicationName,
302 '''<qt><p>Do you want to commit the following file(s):</p><blockquote>''' +
303 '<br>'.join(selFileNames) +
304 '''</blockquote><p>with the commit message:</p><blockquote><pre>''' +
305 str(qt.QStyleSheet.escape(commitMsg)) + '</pre></blockquote></qt>',
306 '&Yes', '&No')):
307 return
308 else:
309 try:
310 scm.doCommit(keepFiles, commitFiles, commitMsg)
311 except CommitError, e:
312 qt.QMessageBox.warning(self, "Commit Failed - " + applicationName,
313 "Commit failed during " + e.operation + ": " + e.msg,
314 '&Ok')
315 except OSError, e:
316 qt.QMessageBox.warning(self, "Commit Failed - " + applicationName,
317 "Commit failed: " + e.strerror,
318 '&Ok')
319 else:
320 self.cmitItem.file.textW.setText('')
321 self.refreshFiles()
323 def getFileState(self):
324 ret = FileState()
325 cur = self.filesW.currentItem()
326 if cur and cur != self.cmitItem:
327 ret.current = self.filesW.currentItem().file.srcName
328 else:
329 ret.current = None
330 ret.selected = {}
332 for x in self.filesW:
333 if x.isSelected():
334 ret.selected[x.file.srcName] = True
335 return ret
337 def restoreFileState(self, state):
338 for x in self.filesW:
339 if state.selected.has_key(x.file.srcName):
340 x.setSelected(True)
341 if x.file.srcName == state.current:
342 self.filesW.setCurrentItem(x)
344 def newTextEdit(self):
345 ret = qt.QTextEdit()
346 self.text.addWidget(ret)
347 return ret
349 def setFiles(self, files):
350 state = self.getFileState()
351 self.filesW.clear()
352 self.createCmitItem()
353 for f in self.files:
354 self.text.removeWidget(f.textW)
356 self.files = []
357 for f in files:
358 f.textW = self.newTextEdit()
359 f.textW.setReadOnly(False)
360 f.textW.setTextFormat(Qt.RichText)
361 f.textW.setText(formatPatchRichText(f.patch, self.patchColors))
362 self.files.append(f)
363 self.filesW.insertItem(MyListItem(self.filesW, f))
365 self.filesW.setCurrentItem(self.cmitItem)
367 # For some reason the currentChanged signal isn't emitted
368 # here. We call currentChange ourselves instead.
369 self.currentChange(self.cmitItem)
371 self.restoreFileState(state)
373 def refreshFiles(self, ignored=None):
374 files = scm.getFiles()
375 if settings.quitOnNoChanges and len(files) == 0:
376 self.close()
377 else:
378 self.setFiles(files)
380 return len(files) > 0
382 def selectAll(self):
383 for x in self.filesW:
384 x.setSelected(True)
386 def unselectAll(self):
387 for x in self.filesW:
388 x.setSelected(False)
390 def showPrefs(self):
391 settings.showSettings()
393 commitMsgRE = re.compile('[ \t\r\f\v]*\n\\s*\n')
394 def fixCommitMsgWhiteSpace(msg):
395 msg = msg.lstrip()
396 msg = msg.rstrip()
397 msg = re.sub(commitMsgRE, '\n\n', msg)
398 msg += '\n'
399 return msg
401 def formatPatchRichText(patch, colors):
402 ret = ['<qt><pre><font color="', colors['std'], '">']
403 prev = ' '
404 for l in patch.split('\n'):
405 if len(l) > 0:
406 c = l[0]
407 else:
408 c = ' '
410 if c != prev:
411 if c == '+': style = 'new'
412 elif c == '-': style = 'remove'
413 elif c == '@': style = 'head'
414 else: style = 'std'
415 ret.extend(['</font><font color="', colors[style], '">'])
416 prev = c
417 line = qt.QStyleSheet.escape(l).ascii()
418 if not line:
419 line = ''
420 else:
421 line = str(line)
422 ret.extend([line, '\n'])
423 ret.append('</pre></qt>')
424 return ''.join(ret)
426 def getEditor():
427 if os.environ.has_key('GCT_EDITOR'):
428 return os.environ['GCT_EDITOR']
429 elif os.environ.has_key('EDITOR'):
430 return os.environ['EDITOR']
431 else:
432 return None
434 scm.repoValid()
436 app = qt.QApplication(sys.argv)
437 settings = settings.Settings()
439 mw = MainWidget()
440 if not mw.refreshFiles() and settings.quitOnNoChanges:
441 print 'No outstanding changes'
442 sys.exit(0)
444 mw.resize(settings.width, settings.height)
446 # The following code doesn't work correctly in some (at least
447 # Metacity) window
448 # managers. http://doc.trolltech.com/3.3/geometry.html contains some
449 # information about this issue.
450 # mw.move(settings.readNumEntry('x', 100)[0],
451 # settings.readNumEntry('y', 100)[0])
453 mw.show()
454 app.setMainWidget(mw)
457 # Handle CTRL-C appropriately
458 signal.signal(signal.SIGINT, lambda s, f: app.quit())
460 ret = app.exec_loop()
461 settings.writeSettings()
462 sys.exit(ret)