qt: Add a dialog for choosing from existing references
[git-cola.git] / cola / qt.py
blob3aaa16a5ab65702e75beff775fe93a56728828f5
1 import re
2 import subprocess
4 from PyQt4 import QtGui
5 from PyQt4 import QtCore
6 from PyQt4.QtCore import Qt
7 from PyQt4.QtCore import SIGNAL
8 from PyQt4.QtGui import QFont
9 from PyQt4.QtGui import QSyntaxHighlighter
10 from PyQt4.QtGui import QTextCharFormat
11 from PyQt4.QtGui import QColor
13 import cola
14 from cola import utils
15 from cola import qtutils
16 from cola.compat import set
17 from cola.qtutils import tr
18 from cola.widgets import defs
21 def create_button(text='', layout=None, tooltip=None, icon=None):
22 """Create a button, set its title, and add it to the parent."""
23 button = QtGui.QPushButton()
24 if text:
25 button.setText(tr(text))
26 if icon:
27 button.setIcon(icon)
28 if tooltip is not None:
29 button.setToolTip(tooltip)
30 if layout is not None:
31 layout.addWidget(button)
32 return button
35 class DockTitleBarWidget(QtGui.QWidget):
36 def __init__(self, parent, title):
37 QtGui.QWidget.__init__(self, parent)
38 self.label = label = QtGui.QLabel()
39 font = label.font()
40 font.setCapitalization(QtGui.QFont.SmallCaps)
41 label.setFont(font)
42 label.setText(title)
44 self.close_button = QtGui.QPushButton()
45 self.close_button.setFlat(True)
46 self.close_button.setFixedSize(QtCore.QSize(16, 16))
47 self.close_button.setIcon(qtutils.titlebar_close_icon())
49 self.toggle_button = QtGui.QPushButton()
50 self.toggle_button.setFlat(True)
51 self.toggle_button.setFixedSize(QtCore.QSize(16, 16))
52 self.toggle_button.setIcon(qtutils.titlebar_normal_icon())
54 self.corner_layout = QtGui.QHBoxLayout()
55 self.corner_layout.setMargin(0)
56 self.corner_layout.setSpacing(defs.spacing)
58 layout = QtGui.QHBoxLayout()
59 layout.setMargin(2)
60 layout.setSpacing(defs.spacing)
61 layout.addWidget(label)
62 layout.addStretch()
63 layout.addLayout(self.corner_layout)
64 layout.addWidget(self.toggle_button)
65 layout.addWidget(self.close_button)
66 self.setLayout(layout)
68 self.connect(self.toggle_button, SIGNAL('clicked()'),
69 self.toggle_floating)
71 self.connect(self.close_button, SIGNAL('clicked()'),
72 self.parent().toggleViewAction().trigger)
74 def toggle_floating(self):
75 self.parent().setFloating(not self.parent().isFloating())
77 def set_title(self, title):
78 self.label.setText(title)
80 def add_corner_widget(self, widget):
81 self.corner_layout.addWidget(widget)
84 def create_dock(title, parent):
85 """Create a dock widget and set it up accordingly."""
86 dock = QtGui.QDockWidget(parent)
87 dock.setWindowTitle(tr(title))
88 dock.setObjectName(title)
89 titlebar = DockTitleBarWidget(dock, title)
90 dock.setTitleBarWidget(titlebar)
91 return dock
94 def create_menu(title, parent):
95 """Create a menu and set its title."""
96 qmenu = QtGui.QMenu(parent)
97 qmenu.setTitle(tr(title))
98 return qmenu
101 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
102 button = QtGui.QToolButton()
103 button.setAutoRaise(True)
104 button.setAutoFillBackground(True)
105 if icon:
106 button.setIcon(icon)
107 if text:
108 button.setText(tr(text))
109 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
110 if tooltip:
111 button.setToolTip(tr(tooltip))
112 if layout is not None:
113 layout.addWidget(button)
114 return button
117 class QFlowLayoutWidget(QtGui.QWidget):
119 _horizontal = QtGui.QBoxLayout.LeftToRight
120 _vertical = QtGui.QBoxLayout.TopToBottom
122 def __init__(self, parent):
123 QtGui.QWidget.__init__(self, parent)
124 self._direction = self._vertical
125 self._layout = layout = QtGui.QBoxLayout(self._direction)
126 layout.setSpacing(defs.spacing)
127 layout.setMargin(defs.margin)
128 self.setLayout(layout)
129 policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
130 QtGui.QSizePolicy.Minimum)
131 self.setSizePolicy(policy)
132 self.setMinimumSize(QtCore.QSize(1, 1))
134 def resizeEvent(self, event):
135 size = event.size()
136 if size.width() * 0.8 < size.height():
137 dxn = self._vertical
138 else:
139 dxn = self._horizontal
141 if dxn != self._direction:
142 self._direction = dxn
143 self.layout().setDirection(dxn)
146 class ExpandableGroupBox(QtGui.QGroupBox):
147 def __init__(self, parent=None):
148 QtGui.QGroupBox.__init__(self, parent)
149 self.setFlat(True)
150 self.expanded = True
151 self.click_pos = None
152 self.arrow_icon_size = 16
154 def set_expanded(self, expanded):
155 if expanded == self.expanded:
156 self.emit(SIGNAL('expanded(bool)'), expanded)
157 return
158 self.expanded = expanded
159 for widget in self.findChildren(QtGui.QWidget):
160 widget.setHidden(not expanded)
161 self.emit(SIGNAL('expanded(bool)'), expanded)
163 def mousePressEvent(self, event):
164 if event.button() == QtCore.Qt.LeftButton:
165 option = QtGui.QStyleOptionGroupBox()
166 self.initStyleOption(option)
167 icon_size = self.arrow_icon_size
168 button_area = QtCore.QRect(0, 0, icon_size, icon_size)
169 offset = self.arrow_icon_size + defs.spacing
170 adjusted = option.rect.adjusted(0, 0, -offset, 0)
171 top_left = adjusted.topLeft()
172 button_area.moveTopLeft(QtCore.QPoint(top_left))
173 self.click_pos = event.pos()
174 QtGui.QGroupBox.mousePressEvent(self, event)
176 def mouseReleaseEvent(self, event):
177 if (event.button() == QtCore.Qt.LeftButton and
178 self.click_pos == event.pos()):
179 self.set_expanded(not self.expanded)
180 QtGui.QGroupBox.mouseReleaseEvent(self, event)
182 def paintEvent(self, event):
183 painter = QtGui.QStylePainter(self)
184 option = QtGui.QStyleOptionGroupBox()
185 self.initStyleOption(option)
186 painter.save()
187 painter.translate(self.arrow_icon_size + defs.spacing, 0)
188 painter.drawText(option.rect, QtCore.Qt.AlignLeft, self.title())
189 painter.restore()
191 style = QtGui.QStyle
192 point = option.rect.adjusted(0, -4, 0, 0).topLeft()
193 icon_size = self.arrow_icon_size
194 option.rect = QtCore.QRect(point.x(), point.y(), icon_size, icon_size)
195 if self.expanded:
196 painter.drawPrimitive(style.PE_IndicatorArrowDown, option)
197 else:
198 painter.drawPrimitive(style.PE_IndicatorArrowRight, option)
201 class GitRefCompleter(QtGui.QCompleter):
202 """Provides completion for branches and tags"""
203 def __init__(self, parent):
204 QtGui.QCompleter.__init__(self, parent)
205 self._model = GitRefModel(parent)
206 self.setModel(self._model)
207 self.setCompletionMode(self.UnfilteredPopupCompletion)
208 self.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
210 def __del__(self):
211 self.dispose()
213 def dispose(self):
214 self._model.dispose()
217 class GitRefLineEdit(QtGui.QLineEdit):
218 def __init__(self, parent=None):
219 QtGui.QLineEdit.__init__(self, parent)
220 self.refcompleter = GitRefCompleter(self)
221 self.setCompleter(self.refcompleter)
223 self.refcompleter.popup().installEventFilter(self)
225 def eventFilter(self, obj, event):
226 """Fix an annoyance on OS X
228 The completer popup steals focus. Work around it.
229 This affects dialogs without QtCore.Qt.WindowModal modality.
232 if obj == self.refcompleter.popup():
233 if event.type() == QtCore.QEvent.FocusIn:
234 return True
235 return False
237 def mouseReleaseEvent(self, event):
238 super(GitRefLineEdit, self).mouseReleaseEvent(event)
239 self.refcompleter.complete()
242 class GitRefDialog(QtGui.QDialog):
243 def __init__(self, title, button_text, parent):
244 super(GitRefDialog, self).__init__(parent)
245 self.setWindowTitle(title)
247 self.label = QtGui.QLabel()
248 self.label.setText(title)
250 self.lineedit = GitRefLineEdit(self)
251 self.setFocusProxy(self.lineedit)
253 self.ok_button = QtGui.QPushButton()
254 self.ok_button.setText(self.tr(button_text))
255 self.ok_button.setIcon(qtutils.apply_icon())
257 self.close_button = QtGui.QPushButton()
258 self.close_button.setText(self.tr('Close'))
260 self.button_layout = QtGui.QHBoxLayout()
261 self.button_layout.setMargin(0)
262 self.button_layout.setSpacing(defs.button_spacing)
263 self.button_layout.addStretch()
264 self.button_layout.addWidget(self.ok_button)
265 self.button_layout.addWidget(self.close_button)
267 self.main_layout = QtGui.QVBoxLayout()
268 self.main_layout.setMargin(defs.margin)
269 self.main_layout.setSpacing(defs.spacing)
271 self.main_layout.addWidget(self.label)
272 self.main_layout.addWidget(self.lineedit)
273 self.main_layout.addLayout(self.button_layout)
274 self.setLayout(self.main_layout)
276 qtutils.connect_button(self.ok_button, self.accept)
277 qtutils.connect_button(self.close_button, self.reject)
279 self.connect(self.lineedit, SIGNAL('textChanged(QString)'),
280 self.text_changed)
282 self.setWindowModality(QtCore.Qt.WindowModal)
283 self.ok_button.setEnabled(False)
285 def text(self):
286 return unicode(self.lineedit.text())
288 def text_changed(self, txt):
289 self.ok_button.setEnabled(bool(self.text()))
291 @staticmethod
292 def ref(title, button_text, parent):
293 dlg = GitRefDialog(title, button_text, parent)
294 dlg.show()
295 dlg.raise_()
296 dlg.setFocus()
297 if dlg.exec_() == GitRefDialog.Accepted:
298 return dlg.text()
299 else:
300 return None
303 class GitRefModel(QtGui.QStandardItemModel):
304 def __init__(self, parent):
305 QtGui.QStandardItemModel.__init__(self, parent)
306 self.cmodel = cola.model()
307 msg = self.cmodel.message_updated
308 self.cmodel.add_observer(msg, self.update_matches)
309 self.update_matches()
311 def dispose(self):
312 self.cmodel.remove_observer(self.update_matches)
314 def update_matches(self):
315 model = self.cmodel
316 matches = model.local_branches + model.remote_branches + model.tags
317 QStandardItem = QtGui.QStandardItem
318 self.clear()
319 for match in matches:
320 item = QStandardItem()
321 item.setIcon(qtutils.git_icon())
322 item.setText(match)
323 self.appendRow(item)
326 class UpdateGitLogCompletionModelThread(QtCore.QThread):
327 def __init__(self, model):
328 QtCore.QThread.__init__(self)
329 self.model = model
330 self.case_insensitive = False
332 def run(self):
333 text = None
334 # Loop when the matched text changes between the start and end time.
335 # This happens when gather_matches() takes too long and the
336 # model's matched_text changes in-between.
337 while text != self.model.matched_text:
338 text = self.model.matched_text
339 items = self.model.gather_matches(self.case_insensitive)
340 self.emit(SIGNAL('items_gathered'), *items)
343 class GitLogCompletionModel(QtGui.QStandardItemModel):
344 def __init__(self, parent):
345 self.matched_text = None
346 QtGui.QStandardItemModel.__init__(self, parent)
347 self.cmodel = cola.model()
348 self.update_thread = UpdateGitLogCompletionModelThread(self)
349 self.connect(self.update_thread, SIGNAL('items_gathered'),
350 self.apply_matches)
352 def lower_cmp(self, a, b):
353 return cmp(a.replace('.','').lower(), b.replace('.','').lower())
355 def update_matches(self, case_insensitive):
356 self.update_thread.case_insensitive = case_insensitive
357 if not self.update_thread.isRunning():
358 self.update_thread.start()
360 def gather_matches(self, case_sensitive):
361 file_list = self.cmodel.everything()
362 files = set(file_list)
363 files_and_dirs = utils.add_parents(set(files))
365 dirs = files_and_dirs.difference(files)
367 model = self.cmodel
368 refs = model.local_branches + model.remote_branches + model.tags
369 matched_text = self.matched_text
371 if matched_text:
372 if case_sensitive:
373 matched_refs = [r for r in refs if matched_text in r]
374 else:
375 matched_refs = [r for r in refs
376 if matched_text.lower() in r.lower()]
377 else:
378 matched_refs = refs
380 matched_refs.sort(cmp=self.lower_cmp)
382 if matched_text:
383 if case_sensitive:
384 matched_paths = [f for f in files_and_dirs
385 if matched_text in f]
386 else:
387 matched_paths = [f for f in files_and_dirs
388 if matched_text.lower() in f.lower()]
389 else:
390 matched_paths = list(files_and_dirs)
392 matched_paths.sort(cmp=self.lower_cmp)
394 return (matched_refs, matched_paths, dirs)
397 def apply_matches(self, matched_refs, matched_paths, dirs):
398 QStandardItem = QtGui.QStandardItem
399 file_icon = qtutils.file_icon()
400 dir_icon = qtutils.dir_icon()
401 git_icon = qtutils.git_icon()
403 matched_text = self.matched_text
404 items = []
405 for ref in matched_refs:
406 item = QStandardItem()
407 item.setText(ref)
408 item.setIcon(git_icon)
409 items.append(item)
411 if matched_paths and (not matched_text or matched_text in '--'):
412 item = QStandardItem()
413 item.setText('--')
414 item.setIcon(file_icon)
415 items.append(item)
417 for match in matched_paths:
418 item = QStandardItem()
419 item.setText(match)
420 if match in dirs:
421 item.setIcon(dir_icon)
422 else:
423 item.setIcon(file_icon)
424 items.append(item)
426 self.clear()
427 self.invisibleRootItem().appendRows(items)
429 def set_match_text(self, text, case_sensitive):
430 self.matched_text = text
431 self.update_matches(case_sensitive)
434 class GitLogLineEdit(QtGui.QLineEdit):
435 def __init__(self, parent=None):
436 QtGui.QLineEdit.__init__(self, parent)
437 # used to hide the completion popup after a drag-select
438 self._drag = 0
440 self._model = GitLogCompletionModel(self)
441 self._delegate = HighlightCompletionDelegate(self)
443 self._completer = QtGui.QCompleter(self)
444 self._completer.setWidget(self)
445 self._completer.setModel(self._model)
446 self._completer.setCompletionMode(
447 QtGui.QCompleter.UnfilteredPopupCompletion)
448 self._completer.popup().setItemDelegate(self._delegate)
450 self.connect(self._completer, SIGNAL('activated(QString)'),
451 self._complete)
452 self.connect(self, SIGNAL('textChanged(QString)'), self._text_changed)
453 self._keys_to_ignore = set([QtCore.Qt.Key_Enter,
454 QtCore.Qt.Key_Return,
455 QtCore.Qt.Key_Escape])
457 def is_case_sensitive(self, text):
458 return bool([char for char in text if char.isupper()])
460 def _text_changed(self, text):
461 text = self.last_word()
462 case_sensitive = self.is_case_sensitive(text)
463 if case_sensitive:
464 self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
465 else:
466 self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
467 self._delegate.set_highlight_text(text, case_sensitive)
468 self._model.set_match_text(text, case_sensitive)
470 def update_matches(self):
471 text = self.last_word()
472 case_sensitive = self.is_case_sensitive(text)
473 self._model.update_matches(case_sensitive)
475 def _complete(self, completion):
477 This is the event handler for the QCompleter.activated(QString) signal,
478 it is called when the user selects an item in the completer popup.
480 if not completion:
481 return
482 words = self.words()
483 if words:
484 words.pop()
485 words.append(unicode(completion))
486 self.setText(subprocess.list2cmdline(words))
487 self.emit(SIGNAL('ref_changed'))
489 def words(self):
490 return utils.shell_usplit(unicode(self.text()))
492 def last_word(self):
493 words = self.words()
494 if not words:
495 return unicode(self.text())
496 if not words[-1]:
497 return u''
498 return words[-1]
500 def event(self, event):
501 if event.type() == QtCore.QEvent.KeyPress:
502 if (event.key() == QtCore.Qt.Key_Tab and
503 self._completer.popup().isVisible()):
504 event.ignore()
505 return True
506 return QtGui.QLineEdit.event(self, event)
508 def do_completion(self):
509 self._completer.popup().setCurrentIndex(
510 self._completer.completionModel().index(0,0))
511 self._completer.complete()
513 def keyPressEvent(self, event):
514 if self._completer.popup().isVisible():
515 if event.key() in self._keys_to_ignore:
516 event.ignore()
517 self._complete(self.last_word())
518 return
520 elif (event.key() == QtCore.Qt.Key_Down and
521 self._completer.completionCount() > 0):
522 event.accept()
523 self.do_completion()
524 return
526 QtGui.QLineEdit.keyPressEvent(self, event)
528 prefix = self.last_word()
529 if prefix != unicode(self._completer.completionPrefix()):
530 self._update_popup_items(prefix)
531 if len(event.text()) > 0 and len(prefix) > 0:
532 self._completer.complete()
533 if len(prefix) == 0:
534 self._completer.popup().hide()
536 #: _drag: 0 - unclicked, 1 - clicked, 2 - dragged
537 def mousePressEvent(self, event):
538 self._drag = 1
539 return QtGui.QLineEdit.mousePressEvent(self, event)
541 def mouseMoveEvent(self, event):
542 if self._drag == 1:
543 self._drag = 2
544 return QtGui.QLineEdit.mouseMoveEvent(self, event)
546 def mouseReleaseEvent(self, event):
547 if self._drag != 2 and event.buttons() != QtCore.Qt.RightButton:
548 self.do_completion()
549 self._drag = 0
550 return QtGui.QLineEdit.mouseReleaseEvent(self, event)
552 def close_popup(self):
553 self._completer.popup().close()
555 def _update_popup_items(self, prefix):
557 Filters the completer's popup items to only show items
558 with the given prefix.
560 self._completer.setCompletionPrefix(prefix)
561 self._completer.popup().setCurrentIndex(
562 self._completer.completionModel().index(0,0))
565 class HighlightCompletionDelegate(QtGui.QStyledItemDelegate):
566 """A delegate used for auto-completion to give formatted completion"""
567 def __init__(self, parent=None): # model, parent=None):
568 QtGui.QStyledItemDelegate.__init__(self, parent)
569 self.highlight_text = ''
570 self.case_sensitive = False
572 self.doc = QtGui.QTextDocument()
573 self.doc.setDocumentMargin(0)
575 def set_highlight_text(self, text, case_sensitive):
576 """Sets the text that will be made bold in the term name when displayed"""
577 self.highlight_text = text
578 self.case_sensitive = case_sensitive
580 def paint(self, painter, option, index):
581 """Overloaded Qt method for custom painting of a model index"""
582 if not self.highlight_text:
583 return QtGui.QStyledItemDelegate.paint(self, painter, option, index)
585 text = unicode(index.data().toPyObject())
586 if self.case_sensitive:
587 html = text.replace(self.highlight_text,
588 '<strong>%s</strong>' % self.highlight_text)
589 else:
590 match = re.match('(.*)(' + self.highlight_text + ')(.*)',
591 text, re.IGNORECASE)
592 if match:
593 start = match.group(1) or ''
594 middle = match.group(2) or ''
595 end = match.group(3) or ''
596 html = (start + ('<strong>%s</strong>' % middle) + end)
597 else:
598 html = text
599 self.doc.setHtml(html)
601 # Painting item without text, Text Document will paint the text
602 optionV4 = QtGui.QStyleOptionViewItemV4(option)
603 self.initStyleOption(optionV4, index)
604 optionV4.text = QtCore.QString()
606 style = QtGui.QApplication.style()
607 style.drawControl(QtGui.QStyle.CE_ItemViewItem, optionV4, painter)
608 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
610 # Highlighting text if item is selected
611 if (optionV4.state & QtGui.QStyle.State_Selected):
612 ctx.palette.setColor(QtGui.QPalette.Text, optionV4.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
614 # translate the painter to where the text is drawn
615 textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, optionV4)
616 painter.save()
618 start = textRect.topLeft() + QtCore.QPoint(3, 0)
619 painter.translate(start)
620 painter.setClipRect(textRect.translated(-start))
622 # tell the text document to draw the html for us
623 self.doc.documentLayout().draw(painter, ctx)
624 painter.restore()
626 # Syntax highlighting
628 def TERMINAL(pattern):
630 Denotes that a pattern is the final pattern that should
631 be matched. If this pattern matches no other formats
632 will be applied, even if they would have matched.
634 return '__TERMINAL__:%s' % pattern
636 # Cache the results of re.compile so that we don't keep
637 # rebuilding the same regexes whenever stylesheets change
638 _RGX_CACHE = {}
640 def rgba(r, g, b, a=255):
641 c = QColor()
642 c.setRgb(r, g, b)
643 c.setAlpha(a)
644 return c
646 default_colors = {
647 'color_text': rgba(0x00, 0x00, 0x00),
648 'color_add': rgba(0xcd, 0xff, 0xe0),
649 'color_remove': rgba(0xff, 0xd0, 0xd0),
650 'color_header': rgba(0xbb, 0xbb, 0xbb),
654 class GenericSyntaxHighligher(QSyntaxHighlighter):
655 def __init__(self, doc, *args, **kwargs):
656 QSyntaxHighlighter.__init__(self, doc)
657 for attr, val in default_colors.items():
658 setattr(self, attr, val)
659 self._rules = []
660 self.generate_rules()
662 def generate_rules(self):
663 pass
665 def create_rules(self, *rules):
666 if len(rules) % 2:
667 raise Exception('create_rules requires an even '
668 'number of arguments.')
669 for idx, rule in enumerate(rules):
670 if idx % 2:
671 continue
672 formats = rules[idx+1]
673 terminal = rule.startswith(TERMINAL(''))
674 if terminal:
675 rule = rule[len(TERMINAL('')):]
676 try:
677 regex = _RGX_CACHE[rule]
678 except KeyError:
679 regex = _RGX_CACHE[rule] = re.compile(rule)
680 self._rules.append((regex, formats, terminal,))
682 def formats(self, line):
683 matched = []
684 for regex, fmts, terminal in self._rules:
685 match = regex.match(line)
686 if not match:
687 continue
688 matched.append([match, fmts])
689 if terminal:
690 return matched
691 return matched
693 def mkformat(self, fg=None, bg=None, bold=False):
694 fmt = QTextCharFormat()
695 if fg:
696 fmt.setForeground(fg)
697 if bg:
698 fmt.setBackground(bg)
699 if bold:
700 fmt.setFontWeight(QFont.Bold)
701 return fmt
703 def highlightBlock(self, qstr):
704 ascii = unicode(qstr)
705 if not ascii:
706 return
707 formats = self.formats(ascii)
708 if not formats:
709 return
710 for match, fmts in formats:
711 start = match.start()
712 groups = match.groups()
714 # No groups in the regex, assume this is a single rule
715 # that spans the entire line
716 if not groups:
717 self.setFormat(0, len(ascii), fmts)
718 continue
720 # Groups exist, rule is a tuple corresponding to group
721 for grpidx, group in enumerate(groups):
722 # allow empty matches
723 if not group:
724 continue
725 # allow None as a no-op format
726 length = len(group)
727 if fmts[grpidx]:
728 self.setFormat(start, start+length,
729 fmts[grpidx])
730 start += length
732 def set_colors(self, colordict):
733 for attr, val in colordict.items():
734 setattr(self, attr, val)
737 class DiffSyntaxHighlighter(GenericSyntaxHighligher):
738 """Implements the diff syntax highlighting
740 This class is used by widgets that display diffs.
743 def __init__(self, doc, whitespace=True):
744 self.whitespace = whitespace
745 GenericSyntaxHighligher.__init__(self, doc)
747 def generate_rules(self):
748 diff_head = self.mkformat(fg=self.color_header)
749 diff_head_bold = self.mkformat(fg=self.color_header, bold=True)
751 diff_add = self.mkformat(fg=self.color_text, bg=self.color_add)
752 diff_remove = self.mkformat(fg=self.color_text, bg=self.color_remove)
754 if self.whitespace:
755 bad_ws = self.mkformat(fg=Qt.black, bg=Qt.red)
757 # We specify the whitespace rule last so that it is
758 # applied after the diff addition/removal rules.
759 # The rules for the header
760 diff_old_rgx = TERMINAL(r'^--- ')
761 diff_new_rgx = TERMINAL(r'^\+\+\+ ')
762 diff_ctx_rgx = TERMINAL(r'^@@ ')
764 diff_hd1_rgx = TERMINAL(r'^diff --git a/.*b/.*')
765 diff_hd2_rgx = TERMINAL(r'^index \S+\.\.\S+')
766 diff_hd3_rgx = TERMINAL(r'^new file mode')
767 diff_hd4_rgx = TERMINAL(r'^deleted file mode')
768 diff_add_rgx = TERMINAL(r'^\+')
769 diff_rmv_rgx = TERMINAL(r'^-')
770 diff_bar_rgx = TERMINAL(r'^([ ]+.*)(\|[ ]+\d+[ ]+[+-]+)$')
771 diff_sts_rgx = (r'(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
772 diff_sum_rgx = (r'(\s+\d+ files changed[^\d]*)'
773 r'(:?\d+ insertions[^\d]*)'
774 r'(:?\d+ deletions.*)$')
776 self.create_rules(diff_old_rgx, diff_head,
777 diff_new_rgx, diff_head,
778 diff_ctx_rgx, diff_head_bold,
779 diff_bar_rgx, (diff_head_bold, diff_head),
780 diff_hd1_rgx, diff_head,
781 diff_hd2_rgx, diff_head,
782 diff_hd3_rgx, diff_head,
783 diff_hd4_rgx, diff_head,
784 diff_add_rgx, diff_add,
785 diff_rmv_rgx, diff_remove,
786 diff_sts_rgx, (None, diff_head,
787 None, diff_head,
788 diff_head),
789 diff_sum_rgx, (diff_head,
790 diff_head,
791 diff_head))
792 if self.whitespace:
793 self.create_rules('(..*?)(\s+)$', (None, bad_ws))
796 if __name__ == '__main__':
797 import sys
798 class SyntaxTestDialog(QtGui.QDialog):
799 def __init__(self, parent):
800 QtGui.QDialog.__init__(self, parent)
801 self.resize(720, 512)
802 self.vboxlayout = QtGui.QVBoxLayout(self)
803 self.vboxlayout.setObjectName('vboxlayout')
804 self.output_text = QtGui.QTextEdit(self)
805 font = QtGui.QFont()
806 if utils.is_darwin():
807 family = 'Monaco'
808 else:
809 family = 'Monospace'
810 font.setFamily(family)
811 font.setPointSize(12)
812 self.output_text.setFont(font)
813 self.output_text.setAcceptDrops(False)
814 self.vboxlayout.addWidget(self.output_text)
815 self.syntax = DiffSyntaxHighlighter(self.output_text.document())
817 app = QtGui.QApplication(sys.argv)
818 dialog = SyntaxTestDialog(qtutils.active_window())
819 dialog.show()
820 dialog.raise_()
821 app.exec_()