dag: Use a slightly thinner edge thickness
[git-cola.git] / cola / qt.py
bloba598fe3e814473932b6ede4340785d246f8e7016
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, provider=None):
204 QtGui.QCompleter.__init__(self, parent)
205 self._model = GitRefModel(parent, provider=provider)
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, provider=None):
219 QtGui.QLineEdit.__init__(self, parent)
220 self.refcompleter = GitRefCompleter(self, provider=provider)
221 self.setCompleter(self.refcompleter)
222 self.refcompleter.popup().installEventFilter(self)
224 def eventFilter(self, obj, event):
225 """Fix an annoyance on OS X
227 The completer popup steals focus. Work around it.
228 This affects dialogs without QtCore.Qt.WindowModal modality.
231 if obj == self.refcompleter.popup():
232 if event.type() == QtCore.QEvent.FocusIn:
233 return True
234 return False
236 def mouseReleaseEvent(self, event):
237 super(GitRefLineEdit, self).mouseReleaseEvent(event)
238 self.refcompleter.complete()
241 class GitRefDialog(QtGui.QDialog):
242 def __init__(self, title, button_text, parent, provider=None):
243 super(GitRefDialog, self).__init__(parent)
244 self.setWindowTitle(title)
246 self.label = QtGui.QLabel()
247 self.label.setText(title)
249 self.lineedit = GitRefLineEdit(self, provider=provider)
250 self.setFocusProxy(self.lineedit)
252 self.ok_button = QtGui.QPushButton()
253 self.ok_button.setText(self.tr(button_text))
254 self.ok_button.setIcon(qtutils.apply_icon())
256 self.close_button = QtGui.QPushButton()
257 self.close_button.setText(self.tr('Close'))
259 self.button_layout = QtGui.QHBoxLayout()
260 self.button_layout.setMargin(0)
261 self.button_layout.setSpacing(defs.button_spacing)
262 self.button_layout.addStretch()
263 self.button_layout.addWidget(self.ok_button)
264 self.button_layout.addWidget(self.close_button)
266 self.main_layout = QtGui.QVBoxLayout()
267 self.main_layout.setMargin(defs.margin)
268 self.main_layout.setSpacing(defs.spacing)
270 self.main_layout.addWidget(self.label)
271 self.main_layout.addWidget(self.lineedit)
272 self.main_layout.addLayout(self.button_layout)
273 self.setLayout(self.main_layout)
275 qtutils.connect_button(self.ok_button, self.accept)
276 qtutils.connect_button(self.close_button, self.reject)
278 self.connect(self.lineedit, SIGNAL('textChanged(QString)'),
279 self.text_changed)
281 self.setWindowModality(QtCore.Qt.WindowModal)
282 self.ok_button.setEnabled(False)
284 def text(self):
285 return unicode(self.lineedit.text())
287 def text_changed(self, txt):
288 self.ok_button.setEnabled(bool(self.text()))
290 @staticmethod
291 def ref(title, button_text, parent, provider=None):
292 dlg = GitRefDialog(title, button_text, parent, provider=provider)
293 dlg.show()
294 dlg.raise_()
295 dlg.setFocus()
296 if dlg.exec_() == GitRefDialog.Accepted:
297 return dlg.text()
298 else:
299 return None
302 class GitRefProvider(QtCore.QObject):
303 def __init__(self, pre=None):
304 super(GitRefProvider, self).__init__()
305 if pre:
306 self.pre = pre
307 else:
308 self.pre = []
309 self.model = model = cola.model()
310 msg = model.message_updated
311 model.add_observer(msg, self.emit_updated)
313 def emit_updated(self):
314 self.emit(SIGNAL('updated()'))
316 def matches(self):
317 model = self.model
318 return self.pre + model.local_branches + model.remote_branches + model.tags
320 def dispose(self):
321 self.model.remove_observer(self.emit_updated)
324 class GitRefModel(QtGui.QStandardItemModel):
325 def __init__(self, parent, provider=None):
326 QtGui.QStandardItemModel.__init__(self, parent)
328 if provider is None:
329 provider = GitRefProvider()
330 self.provider = provider
331 self.update_matches()
333 self.connect(self.provider, SIGNAL('updated()'),
334 self.update_matches)
336 def dispose(self):
337 self.provider.dispose()
339 def update_matches(self):
340 QStandardItem = QtGui.QStandardItem
341 self.clear()
342 for match in self.provider.matches():
343 item = QStandardItem()
344 item.setIcon(qtutils.git_icon())
345 item.setText(match)
346 self.appendRow(item)
349 class UpdateGitLogCompletionModelThread(QtCore.QThread):
350 def __init__(self, model):
351 QtCore.QThread.__init__(self)
352 self.model = model
353 self.case_insensitive = False
355 def run(self):
356 text = None
357 # Loop when the matched text changes between the start and end time.
358 # This happens when gather_matches() takes too long and the
359 # model's matched_text changes in-between.
360 while text != self.model.matched_text:
361 text = self.model.matched_text
362 items = self.model.gather_matches(self.case_insensitive)
363 self.emit(SIGNAL('items_gathered'), *items)
366 class GitLogCompletionModel(QtGui.QStandardItemModel):
367 def __init__(self, parent):
368 self.matched_text = None
369 QtGui.QStandardItemModel.__init__(self, parent)
370 self.cmodel = cola.model()
371 self.update_thread = UpdateGitLogCompletionModelThread(self)
372 self.connect(self.update_thread, SIGNAL('items_gathered'),
373 self.apply_matches)
375 def lower_cmp(self, a, b):
376 return cmp(a.replace('.','').lower(), b.replace('.','').lower())
378 def update_matches(self, case_insensitive):
379 self.update_thread.case_insensitive = case_insensitive
380 if not self.update_thread.isRunning():
381 self.update_thread.start()
383 def gather_matches(self, case_sensitive):
384 file_list = self.cmodel.everything()
385 files = set(file_list)
386 files_and_dirs = utils.add_parents(set(files))
388 dirs = files_and_dirs.difference(files)
390 model = self.cmodel
391 refs = model.local_branches + model.remote_branches + model.tags
392 matched_text = self.matched_text
394 if matched_text:
395 if case_sensitive:
396 matched_refs = [r for r in refs if matched_text in r]
397 else:
398 matched_refs = [r for r in refs
399 if matched_text.lower() in r.lower()]
400 else:
401 matched_refs = refs
403 matched_refs.sort(cmp=self.lower_cmp)
405 if matched_text:
406 if case_sensitive:
407 matched_paths = [f for f in files_and_dirs
408 if matched_text in f]
409 else:
410 matched_paths = [f for f in files_and_dirs
411 if matched_text.lower() in f.lower()]
412 else:
413 matched_paths = list(files_and_dirs)
415 matched_paths.sort(cmp=self.lower_cmp)
417 return (matched_refs, matched_paths, dirs)
420 def apply_matches(self, matched_refs, matched_paths, dirs):
421 QStandardItem = QtGui.QStandardItem
422 file_icon = qtutils.file_icon()
423 dir_icon = qtutils.dir_icon()
424 git_icon = qtutils.git_icon()
426 matched_text = self.matched_text
427 items = []
428 for ref in matched_refs:
429 item = QStandardItem()
430 item.setText(ref)
431 item.setIcon(git_icon)
432 items.append(item)
434 if matched_paths and (not matched_text or matched_text in '--'):
435 item = QStandardItem()
436 item.setText('--')
437 item.setIcon(file_icon)
438 items.append(item)
440 for match in matched_paths:
441 item = QStandardItem()
442 item.setText(match)
443 if match in dirs:
444 item.setIcon(dir_icon)
445 else:
446 item.setIcon(file_icon)
447 items.append(item)
449 self.clear()
450 self.invisibleRootItem().appendRows(items)
452 def set_match_text(self, text, case_sensitive):
453 self.matched_text = text
454 self.update_matches(case_sensitive)
457 class GitLogLineEdit(QtGui.QLineEdit):
458 def __init__(self, parent=None):
459 QtGui.QLineEdit.__init__(self, parent)
460 # used to hide the completion popup after a drag-select
461 self._drag = 0
463 self._model = GitLogCompletionModel(self)
464 self._delegate = HighlightCompletionDelegate(self)
466 self._completer = QtGui.QCompleter(self)
467 self._completer.setWidget(self)
468 self._completer.setModel(self._model)
469 self._completer.setCompletionMode(
470 QtGui.QCompleter.UnfilteredPopupCompletion)
471 self._completer.popup().setItemDelegate(self._delegate)
473 self.connect(self._completer, SIGNAL('activated(QString)'),
474 self._complete)
475 self.connect(self, SIGNAL('textChanged(QString)'), self._text_changed)
476 self._keys_to_ignore = set([QtCore.Qt.Key_Enter,
477 QtCore.Qt.Key_Return,
478 QtCore.Qt.Key_Escape])
480 def is_case_sensitive(self, text):
481 return bool([char for char in text if char.isupper()])
483 def _text_changed(self, text):
484 text = self.last_word()
485 case_sensitive = self.is_case_sensitive(text)
486 if case_sensitive:
487 self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
488 else:
489 self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
490 self._delegate.set_highlight_text(text, case_sensitive)
491 self._model.set_match_text(text, case_sensitive)
493 def update_matches(self):
494 text = self.last_word()
495 case_sensitive = self.is_case_sensitive(text)
496 self._model.update_matches(case_sensitive)
498 def _complete(self, completion):
500 This is the event handler for the QCompleter.activated(QString) signal,
501 it is called when the user selects an item in the completer popup.
503 if not completion:
504 return
505 words = self.words()
506 if words:
507 words.pop()
508 words.append(unicode(completion))
509 self.setText(subprocess.list2cmdline(words))
510 self.emit(SIGNAL('ref_changed'))
512 def words(self):
513 return utils.shell_usplit(unicode(self.text()))
515 def last_word(self):
516 words = self.words()
517 if not words:
518 return unicode(self.text())
519 if not words[-1]:
520 return u''
521 return words[-1]
523 def event(self, event):
524 if event.type() == QtCore.QEvent.KeyPress:
525 if (event.key() == QtCore.Qt.Key_Tab and
526 self._completer.popup().isVisible()):
527 event.ignore()
528 return True
529 return QtGui.QLineEdit.event(self, event)
531 def do_completion(self):
532 self._completer.popup().setCurrentIndex(
533 self._completer.completionModel().index(0,0))
534 self._completer.complete()
536 def keyPressEvent(self, event):
537 if self._completer.popup().isVisible():
538 if event.key() in self._keys_to_ignore:
539 event.ignore()
540 self._complete(self.last_word())
541 return
543 elif (event.key() == QtCore.Qt.Key_Down and
544 self._completer.completionCount() > 0):
545 event.accept()
546 self.do_completion()
547 return
549 QtGui.QLineEdit.keyPressEvent(self, event)
551 prefix = self.last_word()
552 if prefix != unicode(self._completer.completionPrefix()):
553 self._update_popup_items(prefix)
554 if len(event.text()) > 0 and len(prefix) > 0:
555 self._completer.complete()
556 if len(prefix) == 0:
557 self._completer.popup().hide()
559 #: _drag: 0 - unclicked, 1 - clicked, 2 - dragged
560 def mousePressEvent(self, event):
561 self._drag = 1
562 return QtGui.QLineEdit.mousePressEvent(self, event)
564 def mouseMoveEvent(self, event):
565 if self._drag == 1:
566 self._drag = 2
567 return QtGui.QLineEdit.mouseMoveEvent(self, event)
569 def mouseReleaseEvent(self, event):
570 if self._drag != 2 and event.buttons() != QtCore.Qt.RightButton:
571 self.do_completion()
572 self._drag = 0
573 return QtGui.QLineEdit.mouseReleaseEvent(self, event)
575 def close_popup(self):
576 self._completer.popup().close()
578 def _update_popup_items(self, prefix):
580 Filters the completer's popup items to only show items
581 with the given prefix.
583 self._completer.setCompletionPrefix(prefix)
584 self._completer.popup().setCurrentIndex(
585 self._completer.completionModel().index(0,0))
588 class HighlightCompletionDelegate(QtGui.QStyledItemDelegate):
589 """A delegate used for auto-completion to give formatted completion"""
590 def __init__(self, parent=None): # model, parent=None):
591 QtGui.QStyledItemDelegate.__init__(self, parent)
592 self.highlight_text = ''
593 self.case_sensitive = False
595 self.doc = QtGui.QTextDocument()
596 self.doc.setDocumentMargin(0)
598 def set_highlight_text(self, text, case_sensitive):
599 """Sets the text that will be made bold in the term name when displayed"""
600 self.highlight_text = text
601 self.case_sensitive = case_sensitive
603 def paint(self, painter, option, index):
604 """Overloaded Qt method for custom painting of a model index"""
605 if not self.highlight_text:
606 return QtGui.QStyledItemDelegate.paint(self, painter, option, index)
608 text = unicode(index.data().toPyObject())
609 if self.case_sensitive:
610 html = text.replace(self.highlight_text,
611 '<strong>%s</strong>' % self.highlight_text)
612 else:
613 match = re.match('(.*)(' + self.highlight_text + ')(.*)',
614 text, re.IGNORECASE)
615 if match:
616 start = match.group(1) or ''
617 middle = match.group(2) or ''
618 end = match.group(3) or ''
619 html = (start + ('<strong>%s</strong>' % middle) + end)
620 else:
621 html = text
622 self.doc.setHtml(html)
624 # Painting item without text, Text Document will paint the text
625 optionV4 = QtGui.QStyleOptionViewItemV4(option)
626 self.initStyleOption(optionV4, index)
627 optionV4.text = QtCore.QString()
629 style = QtGui.QApplication.style()
630 style.drawControl(QtGui.QStyle.CE_ItemViewItem, optionV4, painter)
631 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
633 # Highlighting text if item is selected
634 if (optionV4.state & QtGui.QStyle.State_Selected):
635 ctx.palette.setColor(QtGui.QPalette.Text, optionV4.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
637 # translate the painter to where the text is drawn
638 textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, optionV4)
639 painter.save()
641 start = textRect.topLeft() + QtCore.QPoint(3, 0)
642 painter.translate(start)
643 painter.setClipRect(textRect.translated(-start))
645 # tell the text document to draw the html for us
646 self.doc.documentLayout().draw(painter, ctx)
647 painter.restore()
649 # Syntax highlighting
651 def TERMINAL(pattern):
653 Denotes that a pattern is the final pattern that should
654 be matched. If this pattern matches no other formats
655 will be applied, even if they would have matched.
657 return '__TERMINAL__:%s' % pattern
659 # Cache the results of re.compile so that we don't keep
660 # rebuilding the same regexes whenever stylesheets change
661 _RGX_CACHE = {}
663 def rgba(r, g, b, a=255):
664 c = QColor()
665 c.setRgb(r, g, b)
666 c.setAlpha(a)
667 return c
669 default_colors = {
670 'color_text': rgba(0x00, 0x00, 0x00),
671 'color_add': rgba(0xcd, 0xff, 0xe0),
672 'color_remove': rgba(0xff, 0xd0, 0xd0),
673 'color_header': rgba(0xbb, 0xbb, 0xbb),
677 class GenericSyntaxHighligher(QSyntaxHighlighter):
678 def __init__(self, doc, *args, **kwargs):
679 QSyntaxHighlighter.__init__(self, doc)
680 for attr, val in default_colors.items():
681 setattr(self, attr, val)
682 self._rules = []
683 self.generate_rules()
685 def generate_rules(self):
686 pass
688 def create_rules(self, *rules):
689 if len(rules) % 2:
690 raise Exception('create_rules requires an even '
691 'number of arguments.')
692 for idx, rule in enumerate(rules):
693 if idx % 2:
694 continue
695 formats = rules[idx+1]
696 terminal = rule.startswith(TERMINAL(''))
697 if terminal:
698 rule = rule[len(TERMINAL('')):]
699 try:
700 regex = _RGX_CACHE[rule]
701 except KeyError:
702 regex = _RGX_CACHE[rule] = re.compile(rule)
703 self._rules.append((regex, formats, terminal,))
705 def formats(self, line):
706 matched = []
707 for regex, fmts, terminal in self._rules:
708 match = regex.match(line)
709 if not match:
710 continue
711 matched.append([match, fmts])
712 if terminal:
713 return matched
714 return matched
716 def mkformat(self, fg=None, bg=None, bold=False):
717 fmt = QTextCharFormat()
718 if fg:
719 fmt.setForeground(fg)
720 if bg:
721 fmt.setBackground(bg)
722 if bold:
723 fmt.setFontWeight(QFont.Bold)
724 return fmt
726 def highlightBlock(self, qstr):
727 ascii = unicode(qstr)
728 if not ascii:
729 return
730 formats = self.formats(ascii)
731 if not formats:
732 return
733 for match, fmts in formats:
734 start = match.start()
735 groups = match.groups()
737 # No groups in the regex, assume this is a single rule
738 # that spans the entire line
739 if not groups:
740 self.setFormat(0, len(ascii), fmts)
741 continue
743 # Groups exist, rule is a tuple corresponding to group
744 for grpidx, group in enumerate(groups):
745 # allow empty matches
746 if not group:
747 continue
748 # allow None as a no-op format
749 length = len(group)
750 if fmts[grpidx]:
751 self.setFormat(start, start+length,
752 fmts[grpidx])
753 start += length
755 def set_colors(self, colordict):
756 for attr, val in colordict.items():
757 setattr(self, attr, val)
760 class DiffSyntaxHighlighter(GenericSyntaxHighligher):
761 """Implements the diff syntax highlighting
763 This class is used by widgets that display diffs.
766 def __init__(self, doc, whitespace=True):
767 self.whitespace = whitespace
768 GenericSyntaxHighligher.__init__(self, doc)
770 def generate_rules(self):
771 diff_head = self.mkformat(fg=self.color_header)
772 diff_head_bold = self.mkformat(fg=self.color_header, bold=True)
774 diff_add = self.mkformat(fg=self.color_text, bg=self.color_add)
775 diff_remove = self.mkformat(fg=self.color_text, bg=self.color_remove)
777 if self.whitespace:
778 bad_ws = self.mkformat(fg=Qt.black, bg=Qt.red)
780 # We specify the whitespace rule last so that it is
781 # applied after the diff addition/removal rules.
782 # The rules for the header
783 diff_old_rgx = TERMINAL(r'^--- ')
784 diff_new_rgx = TERMINAL(r'^\+\+\+ ')
785 diff_ctx_rgx = TERMINAL(r'^@@ ')
787 diff_hd1_rgx = TERMINAL(r'^diff --git a/.*b/.*')
788 diff_hd2_rgx = TERMINAL(r'^index \S+\.\.\S+')
789 diff_hd3_rgx = TERMINAL(r'^new file mode')
790 diff_hd4_rgx = TERMINAL(r'^deleted file mode')
791 diff_add_rgx = TERMINAL(r'^\+')
792 diff_rmv_rgx = TERMINAL(r'^-')
793 diff_bar_rgx = TERMINAL(r'^([ ]+.*)(\|[ ]+\d+[ ]+[+-]+)$')
794 diff_sts_rgx = (r'(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
795 diff_sum_rgx = (r'(\s+\d+ files changed[^\d]*)'
796 r'(:?\d+ insertions[^\d]*)'
797 r'(:?\d+ deletions.*)$')
799 self.create_rules(diff_old_rgx, diff_head,
800 diff_new_rgx, diff_head,
801 diff_ctx_rgx, diff_head_bold,
802 diff_bar_rgx, (diff_head_bold, diff_head),
803 diff_hd1_rgx, diff_head,
804 diff_hd2_rgx, diff_head,
805 diff_hd3_rgx, diff_head,
806 diff_hd4_rgx, diff_head,
807 diff_add_rgx, diff_add,
808 diff_rmv_rgx, diff_remove,
809 diff_sts_rgx, (None, diff_head,
810 None, diff_head,
811 diff_head),
812 diff_sum_rgx, (diff_head,
813 diff_head,
814 diff_head))
815 if self.whitespace:
816 self.create_rules('(..*?)(\s+)$', (None, bad_ws))
819 if __name__ == '__main__':
820 import sys
821 class SyntaxTestDialog(QtGui.QDialog):
822 def __init__(self, parent):
823 QtGui.QDialog.__init__(self, parent)
824 self.resize(720, 512)
825 self.vboxlayout = QtGui.QVBoxLayout(self)
826 self.vboxlayout.setObjectName('vboxlayout')
827 self.output_text = QtGui.QTextEdit(self)
828 font = QtGui.QFont()
829 if utils.is_darwin():
830 family = 'Monaco'
831 else:
832 family = 'Monospace'
833 font.setFamily(family)
834 font.setPointSize(12)
835 self.output_text.setFont(font)
836 self.output_text.setAcceptDrops(False)
837 self.vboxlayout.addWidget(self.output_text)
838 self.syntax = DiffSyntaxHighlighter(self.output_text.document())
840 app = QtGui.QApplication(sys.argv)
841 dialog = SyntaxTestDialog(qtutils.active_window())
842 dialog.show()
843 dialog.raise_()
844 app.exec_()