1 from PyQt4
import QtGui
2 from PyQt4
import QtCore
3 from PyQt4
.QtCore
import SIGNAL
6 from cola
import gitcmds
7 from cola
import signals
8 from cola
.qt
import create_toolbutton
9 from cola
.qtutils
import add_action
10 from cola
.qtutils
import confirm
11 from cola
.qtutils
import connect_action
12 from cola
.qtutils
import connect_action_bool
13 from cola
.qtutils
import connect_button
14 from cola
.qtutils
import emit
15 from cola
.qtutils
import log
16 from cola
.qtutils
import relay_signal
17 from cola
.qtutils
import options_icon
18 from cola
.qtutils
import save_icon
19 from cola
.qtutils
import tr
20 from cola
.widgets
import defs
21 from cola
.prefs
import diff_font
22 from cola
.dag
.model
import DAG
23 from cola
.dag
.model
import RepoReader
24 from cola
.widgets
.selectcommits
import select_commits
27 class CommitMessageEditor(QtGui
.QWidget
):
28 def __init__(self
, model
, parent
):
29 QtGui
.QWidget
.__init
__(self
, parent
)
32 self
.notifying
= False
33 self
.summary_placeholder
= u
'Commit summary'
34 self
.description_placeholder
= u
'Extended description...'
36 # Palette for normal text
37 self
.default_palette
= QtGui
.QPalette(self
.palette())
39 # Palette used for the placeholder text
40 self
.placeholder_palette
= pal
= QtGui
.QPalette(self
.palette())
41 color
= pal
.text().color()
43 pal
.setColor(QtGui
.QPalette
.Active
, QtGui
.QPalette
.Text
, color
)
44 pal
.setColor(QtGui
.QPalette
.Inactive
, QtGui
.QPalette
.Text
, color
)
46 self
.summary
= QtGui
.QLineEdit()
47 self
.description
= CommitMessageTextEdit()
49 self
.commit_button
= create_toolbutton(text
='Commit@@verb',
50 tooltip
='Commit staged changes',
53 self
.actions_menu
= QtGui
.QMenu()
54 self
.actions_button
= create_toolbutton(icon
=options_icon(),
56 self
.actions_button
.setMenu(self
.actions_menu
)
57 self
.actions_button
.setPopupMode(QtGui
.QToolButton
.InstantPopup
)
60 self
.signoff_action
= self
.actions_menu
.addAction(tr('Sign Off'))
61 self
.signoff_action
.setToolTip('Sign off on this commit')
62 self
.signoff_action
.setShortcut('Ctrl+i')
64 self
.commit_action
= self
.actions_menu
.addAction(tr('Commit@@verb'))
65 self
.commit_action
.setToolTip(tr('Commit staged changes'))
66 self
.commit_action
.setShortcut('Ctrl+Return')
68 self
.actions_menu
.addSeparator()
69 self
.amend_action
= self
.actions_menu
.addAction(tr('Amend Last Commit'))
70 self
.amend_action
.setCheckable(True)
72 self
.prev_commits_menu
= self
.actions_menu
.addMenu(
73 'Load Previous Commit Message')
74 self
.connect(self
.prev_commits_menu
, SIGNAL('aboutToShow()'),
75 self
.build_prev_commits_menu
)
77 self
.toplayout
= QtGui
.QHBoxLayout()
78 self
.toplayout
.setMargin(0)
79 self
.toplayout
.setSpacing(defs
.spacing
)
80 self
.toplayout
.addWidget(self
.actions_button
)
81 self
.toplayout
.addWidget(self
.summary
)
82 self
.toplayout
.addWidget(self
.commit_button
)
84 self
.mainlayout
= QtGui
.QVBoxLayout()
85 self
.mainlayout
.setMargin(defs
.margin
)
86 self
.mainlayout
.setSpacing(defs
.spacing
)
87 self
.mainlayout
.addLayout(self
.toplayout
)
88 self
.mainlayout
.addWidget(self
.description
)
89 self
.setLayout(self
.mainlayout
)
91 relay_signal(self
, self
.description
,
92 SIGNAL(signals
.load_previous_message
))
94 connect_button(self
.commit_button
, self
.commit
)
95 connect_action(self
.commit_action
, self
.commit
)
96 connect_action(self
.signoff_action
, emit(self
, signals
.signoff
))
98 cola
.notifier().connect(signals
.amend
, self
.amend_action
.setChecked
)
100 # Broadcast the amend mode
101 connect_action_bool(self
.amend_action
, emit(self
, signals
.amend_mode
))
103 self
.model
.add_message_observer(self
.model
.message_commit_message_changed
,
104 self
.set_commit_message
)
106 self
.connect(self
.summary
, SIGNAL('returnPressed()'),
107 self
.summary_return_pressed
)
109 self
.connect(self
.summary
, SIGNAL('cursorPositionChanged(int,int)'),
110 lambda x
, y
: self
.emit_summary_position())
112 # Keep model informed of changes
113 self
.connect(self
.summary
, SIGNAL('textChanged(QString)'),
114 self
.commit_message_changed
)
116 self
.connect(self
.description
, SIGNAL('textChanged()'),
117 self
.commit_message_changed
)
119 self
.connect(self
.description
, SIGNAL('cursorPositionChanged()'),
120 self
.emit_cursor_position
)
122 self
.connect(self
.description
, SIGNAL('shiftTab()'),
125 self
.setFont(diff_font())
127 self
.summary
.installEventFilter(self
)
128 self
.description
.installEventFilter(self
)
130 self
.enable_placeholder_summary(True)
131 self
.enable_placeholder_description(True)
133 self
.commit_button
.setEnabled(False)
134 self
.commit_action
.setEnabled(False)
136 self
.setFocusProxy(self
.summary
)
138 def focus_summary(self
):
139 self
.summary
.setFocus(True)
141 def is_summary_placeholder(self
):
142 summary
= unicode(self
.summary
.text()).strip()
143 return summary
== self
.summary_placeholder
145 def is_description_placeholder(self
):
146 description
= unicode(self
.description
.toPlainText()).strip()
147 return description
== self
.description_placeholder
149 def eventFilter(self
, obj
, event
):
150 if obj
== self
.summary
:
151 if event
.type() == QtCore
.QEvent
.FocusIn
:
152 self
.emit_summary_position()
153 if self
.is_summary_placeholder():
154 self
.enable_placeholder_summary(False)
156 elif event
.type() == QtCore
.QEvent
.FocusOut
:
157 if not bool(self
.commit_summary()):
158 self
.enable_placeholder_summary(True)
160 elif obj
== self
.description
:
161 if event
.type() == QtCore
.QEvent
.FocusIn
:
162 self
.emit_cursor_position()
163 if self
.is_description_placeholder():
164 self
.enable_placeholder_description(False)
166 elif event
.type() == QtCore
.QEvent
.FocusOut
:
167 if not bool(self
.commit_description()):
168 self
.enable_placeholder_description(True)
172 def enable_placeholder_summary(self
, placeholder
):
173 blocksignals
= self
.summary
.blockSignals(True)
175 self
.summary
.setText(self
.summary_placeholder
)
178 self
.summary
.setCursorPosition(0)
179 self
.summary
.blockSignals(blocksignals
)
180 self
.set_placeholder_palette(self
.summary
, placeholder
)
182 def enable_placeholder_description(self
, placeholder
):
183 blocksignals
= self
.description
.blockSignals(True)
185 self
.description
.setPlainText(self
.description_placeholder
)
187 self
.description
.clear()
188 self
.description
.blockSignals(blocksignals
)
189 self
.set_placeholder_palette(self
.description
, placeholder
)
191 def set_placeholder_palette(self
, widget
, placeholder
):
193 widget
.setPalette(self
.placeholder_palette
)
195 widget
.setPalette(self
.default_palette
)
197 def summary_return_pressed(self
):
198 if bool(self
.commit_summary()):
199 self
.description
.setFocus(True)
201 def commit_summary(self
):
202 """Return the commit summary as unicode"""
203 summary
= unicode(self
.summary
.text()).strip()
204 if summary
!= self
.summary_placeholder
:
209 def commit_description(self
):
210 """Return the commit description as unicode"""
211 description
= unicode(self
.description
.toPlainText()).strip()
212 if description
!= self
.description_placeholder
:
217 def commit_message(self
):
218 """Return the commit message as a unicode string"""
219 summary
= self
.commit_summary()
220 description
= self
.commit_description()
221 if summary
and description
:
222 return summary
+ u
'\n\n' + description
226 return u
'\n\n' + description
230 def commit_message_changed(self
, value
=None):
231 """Update the model when values change"""
232 self
.notifying
= True
233 message
= self
.commit_message()
234 self
.model
.set_commitmsg(message
)
235 self
.update_placeholder_state()
236 self
.notifying
= False
237 self
.update_actions()
239 def update_actions(self
):
240 commit_enabled
= bool(self
.commit_summary())
241 self
.commit_button
.setEnabled(commit_enabled
)
242 self
.commit_action
.setEnabled(commit_enabled
)
244 def update_placeholder_state(self
):
245 """Update the color palette for the placeholder text"""
246 self
.set_placeholder_palette(self
.summary
,
247 self
.is_summary_placeholder())
248 self
.set_placeholder_palette(self
.description
,
249 self
.is_description_placeholder())
251 def set_commit_message(self
, message
):
252 """Set the commit message to match the observed model"""
254 # Calling self.model.set_commitmsg(message) causes us to
255 # loop around so break the loop
258 # Parse the "summary" and "description" fields
259 umsg
= unicode(message
)
260 lines
= umsg
.splitlines()
262 num_lines
= len(lines
)
270 # Message has a summary only
275 # Message has two lines; this is not a common case
277 description
= lines
[1]
280 # Summary and several description lines
283 # We usually skip this line but check just in case
284 description_lines
= lines
[1:]
286 description_lines
= lines
[2:]
287 description
= u
'\n'.join(description_lines
)
289 focus_summary
= not summary
290 focus_description
= not description
293 if not summary
and not self
.summary
.hasFocus():
294 summary
= self
.summary_placeholder
296 blocksignals
= self
.summary
.blockSignals(True)
297 self
.summary
.setText(summary
)
298 self
.summary
.setCursorPosition(0)
299 self
.summary
.blockSignals(blocksignals
)
302 if not description
and not self
.description
.hasFocus():
303 description
= self
.description_placeholder
305 blocksignals
= self
.description
.blockSignals(True)
306 self
.description
.setPlainText(description
)
307 self
.description
.blockSignals(blocksignals
)
310 self
.update_placeholder_state()
312 # Focus the empty summary or description
314 self
.summary
.setFocus(True)
315 self
.emit_summary_position()
316 elif focus_description
:
317 self
.description
.setFocus(True)
318 self
.emit_cursor_position()
320 self
.emit_summary_position()
322 self
.update_actions()
324 def setFont(self
, font
):
325 """Pass the setFont() calls down to the text widgets"""
326 self
.summary
.setFont(font
)
327 self
.description
.setFont(font
)
329 def set_mode(self
, mode
):
330 checked
= (mode
== self
.model
.mode_amend
)
331 blocksignals
= self
.amend_action
.blockSignals(True)
332 self
.amend_action
.setChecked(checked
)
333 self
.amend_action
.blockSignals(blocksignals
)
335 def emit_summary_position(self
):
336 cols
= self
.summary
.cursorPosition()
337 self
.emit(SIGNAL('cursorPosition(int,int)'), 1, cols
)
339 def emit_cursor_position(self
):
340 """Update the UI with the current row and column."""
341 cursor
= self
.description
.textCursor()
342 position
= cursor
.position()
343 txt
= unicode(self
.description
.toPlainText())
344 rows
= txt
[:position
].count('\n') + 1 + 2 # description starts at 2
345 cols
= cursor
.columnNumber()
346 self
.emit(SIGNAL('cursorPosition(int,int)'), rows
, cols
)
349 """Attempt to create a commit from the index and commit message."""
350 if not bool(self
.commit_summary()):
351 # Describe a good commit message
353 'Please supply a commit message.\n\n'
354 'A good commit message has the following format:\n\n'
355 '- First line: Describe in one sentence what you did.\n'
356 '- Second line: Blank\n'
357 '- Remaining lines: Describe why this change is good.\n')
359 cola
.notifier().broadcast(signals
.information
,
360 'Missing Commit Message',
364 msg
= self
.commit_message()
366 if not self
.model
.staged
:
368 'No changes to commit.\n\n'
369 'You must stage at least 1 file before you can commit.')
370 if self
.model
.modified
:
371 informative_text
= tr('Would you like to stage and '
372 'commit all modified files?')
373 if not confirm('Stage and commit?',
381 cola
.notifier().broadcast(signals
.information
,
385 cola
.notifier().broadcast(signals
.stage_modified
)
387 # Warn that amending published commits is generally bad
388 amend
= self
.amend_action
.isChecked()
389 if (amend
and self
.model
.is_commit_published() and
390 not confirm('Rewrite Published Commit?',
391 'This commit has already been published.\n'
392 'This operation will rewrite published history.\n'
393 'You probably don\'t want to do this.',
394 'Amend the published commit?',
396 default
=False, icon
=save_icon())):
399 cola
.notifier().broadcast(signals
.commit
, amend
, msg
)
401 def build_prev_commits_menu(self
):
403 commits
= RepoReader(dag
)
406 for idx
, c
in enumerate(commits
):
407 menu_commits
.insert(0, c
)
411 menu
= self
.prev_commits_menu
413 for c
in menu_commits
:
414 menu
.addAction(c
.summary
,
415 lambda c
=c
: self
.load_previous_message(c
.sha1
))
417 if len(commits
) == 6:
419 menu
.addAction('More...', self
.choose_commit
)
421 def choose_commit(self
):
422 revs
, summaries
= gitcmds
.log_helper()
423 sha1s
= select_commits('Select Commit Message', revs
, summaries
,
428 self
.load_previous_message(sha1
)
430 def load_previous_message(self
, sha1
):
431 self
.emit(SIGNAL(signals
.load_previous_message
), sha1
)
434 class CommitMessageTextEdit(QtGui
.QTextEdit
):
435 def __init__(self
, parent
=None):
436 QtGui
.QTextEdit
.__init
__(self
, parent
)
437 self
.setLineWrapMode(QtGui
.QTextEdit
.NoWrap
)
438 self
.setAcceptRichText(False)
439 self
.setMinimumSize(QtCore
.QSize(1, 1))
441 self
.action_emit_shift_tab
= add_action(self
,
442 'Shift Tab', self
.shift_tab
, 'Shift+tab')
444 self
.installEventFilter(self
)
446 def eventFilter(self
, obj
, event
):
447 if event
.type() == QtCore
.QEvent
.FocusIn
:
448 height
= QtGui
.QFontMetrics(self
.font()).height() * 3
449 height
+= defs
.spacing
* 4
450 self
.setMinimumSize(QtCore
.QSize(1, height
))
452 elif event
.type() == QtCore
.QEvent
.FocusOut
:
453 self
.setMinimumSize(QtCore
.QSize(1, 1))
458 self
.emit(SIGNAL('shiftTab()'))