bookmarks: add context menu actions
[git-cola.git] / cola / qtutils.py
blobd4e798d1f4571bd4b7164ded96bc55e92a637f25
1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
3 """
4 import os
5 import re
7 from PyQt4 import QtGui
8 from PyQt4 import QtCore
9 from PyQt4.QtCore import Qt
10 from PyQt4.QtCore import SIGNAL
12 from cola import core
13 from cola import gitcfg
14 from cola import utils
15 from cola import settings
16 from cola import resources
17 from cola.compat import set
18 from cola.decorators import memoize
19 from cola.i18n import N_
20 from cola.interaction import Interaction
21 from cola.models.prefs import FONTDIFF
22 from cola.widgets import defs
25 def connect_action(action, fn):
26 action.connect(action, SIGNAL('triggered()'), fn)
29 def connect_action_bool(action, fn):
30 action.connect(action, SIGNAL('triggered(bool)'), fn)
33 def connect_button(button, fn):
34 button.connect(button, SIGNAL('clicked()'), fn)
37 def connect_toggle(toggle, fn):
38 toggle.connect(toggle, SIGNAL('toggled(bool)'), fn)
41 def active_window():
42 return QtGui.QApplication.activeWindow()
45 def prompt(msg, title=None, text=''):
46 """Presents the user with an input widget and returns the input."""
47 if title is None:
48 title = msg
49 result = QtGui.QInputDialog.getText(active_window(), msg, title,
50 QtGui.QLineEdit.Normal, text)
51 return (unicode(result[0]), result[1])
54 def create_listwidget_item(text, filename):
55 """Creates a QListWidgetItem with text and the icon at filename."""
56 item = QtGui.QListWidgetItem()
57 item.setIcon(QtGui.QIcon(filename))
58 item.setText(text)
59 return item
62 def create_treewidget_item(text, filename):
63 """Creates a QTreeWidgetItem with text and the icon at filename."""
64 icon = cached_icon_from_path(filename)
65 item = QtGui.QTreeWidgetItem()
66 item.setIcon(0, icon)
67 item.setText(0, text)
68 return item
71 @memoize
72 def cached_icon_from_path(filename):
73 return QtGui.QIcon(filename)
76 def confirm(title, text, informative_text, ok_text,
77 icon=None, default=True):
78 """Confirm that an action should take place"""
79 if icon is None:
80 icon = ok_icon()
81 elif icon and isinstance(icon, basestring):
82 icon = QtGui.QIcon(icon)
83 msgbox = QtGui.QMessageBox(active_window())
84 msgbox.setWindowModality(Qt.WindowModal)
85 msgbox.setWindowTitle(title)
86 msgbox.setText(text)
87 msgbox.setInformativeText(informative_text)
88 ok = msgbox.addButton(ok_text, QtGui.QMessageBox.ActionRole)
89 ok.setIcon(icon)
90 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
91 if default:
92 msgbox.setDefaultButton(ok)
93 else:
94 msgbox.setDefaultButton(cancel)
95 msgbox.exec_()
96 return msgbox.clickedButton() == ok
99 def critical(title, message=None, details=None):
100 """Show a warning with the provided title and message."""
101 if message is None:
102 message = title
103 mbox = QtGui.QMessageBox(active_window())
104 mbox.setWindowTitle(title)
105 mbox.setTextFormat(Qt.PlainText)
106 mbox.setText(message)
107 mbox.setIcon(QtGui.QMessageBox.Critical)
108 mbox.setStandardButtons(QtGui.QMessageBox.Close)
109 mbox.setDefaultButton(QtGui.QMessageBox.Close)
110 if details:
111 mbox.setDetailedText(details)
112 mbox.exec_()
115 def information(title, message=None, details=None, informative_text=None):
116 """Show information with the provided title and message."""
117 if message is None:
118 message = title
119 mbox = QtGui.QMessageBox(active_window())
120 mbox.setStandardButtons(QtGui.QMessageBox.Close)
121 mbox.setDefaultButton(QtGui.QMessageBox.Close)
122 mbox.setWindowTitle(title)
123 mbox.setWindowModality(Qt.WindowModal)
124 mbox.setTextFormat(Qt.PlainText)
125 mbox.setText(message)
126 if informative_text:
127 mbox.setInformativeText(informative_text)
128 if details:
129 mbox.setDetailedText(details)
130 # Render git.svg into a 1-inch wide pixmap
131 pixmap = QtGui.QPixmap(resources.icon('git.svg'))
132 xres = pixmap.physicalDpiX()
133 pixmap = pixmap.scaledToHeight(xres, Qt.SmoothTransformation)
134 mbox.setIconPixmap(pixmap)
135 mbox.exec_()
138 def question(title, msg, default=True):
139 """Launches a QMessageBox question with the provided title and message.
140 Passing "default=False" will make "No" the default choice."""
141 yes = QtGui.QMessageBox.Yes
142 no = QtGui.QMessageBox.No
143 buttons = yes | no
144 if default:
145 default = yes
146 else:
147 default = no
148 result = (QtGui.QMessageBox
149 .question(active_window(), title, msg, buttons, default))
150 return result == QtGui.QMessageBox.Yes
153 def selected_treeitem(tree_widget):
154 """Returns a(id_number, is_selected) for a QTreeWidget."""
155 id_number = None
156 selected = False
157 item = tree_widget.currentItem()
158 if item:
159 id_number = item.data(0, Qt.UserRole).toInt()[0]
160 selected = True
161 return(id_number, selected)
164 def selected_row(list_widget):
165 """Returns a(row_number, is_selected) tuple for a QListWidget."""
166 items = list_widget.selectedItems()
167 if not items:
168 return (-1, False)
169 item = items[0]
170 return (list_widget.row(item), True)
173 def selection_list(listwidget, items):
174 """Returns an array of model items that correspond to
175 the selected QListWidget indices."""
176 selected = []
177 itemcount = listwidget.count()
178 widgetitems = [ listwidget.item(idx) for idx in range(itemcount) ]
180 for item, widgetitem in zip(items, widgetitems):
181 if widgetitem.isSelected():
182 selected.append(item)
183 return selected
186 def tree_selection(treeitem, items):
187 """Returns model items that correspond to selected widget indices"""
188 itemcount = treeitem.childCount()
189 widgetitems = [ treeitem.child(idx) for idx in range(itemcount) ]
190 selected = []
191 for item, widgetitem in zip(items[:len(widgetitems)], widgetitems):
192 if widgetitem.isSelected():
193 selected.append(item)
195 return selected
198 def selected_item(list_widget, items):
199 """Returns the selected item in a QListWidget."""
200 widget_items = list_widget.selectedItems()
201 if not widget_items:
202 return None
203 widget_item = widget_items[0]
204 row = list_widget.row(widget_item)
205 if row < len(items):
206 return items[row]
207 else:
208 return None
211 def selected_items(list_widget, items):
212 """Returns the selected item in a QListWidget."""
213 selection = []
214 widget_items = list_widget.selectedItems()
215 if not widget_items:
216 return selection
217 for widget_item in widget_items:
218 row = list_widget.row(widget_item)
219 if row < len(items):
220 selection.append(items[row])
221 return selection
224 def open_file(title, directory=None):
225 """Creates an Open File dialog and returns a filename."""
226 return unicode(QtGui.QFileDialog
227 .getOpenFileName(active_window(), title, directory))
230 def open_files(title, directory=None, filter=None):
231 """Creates an Open File dialog and returns a list of filenames."""
232 return (QtGui.QFileDialog
233 .getOpenFileNames(active_window(), title, directory, filter))
236 def opendir_dialog(title, path):
237 """Prompts for a directory path"""
239 flags = (QtGui.QFileDialog.ShowDirsOnly |
240 QtGui.QFileDialog.DontResolveSymlinks)
241 return unicode(QtGui.QFileDialog
242 .getExistingDirectory(active_window(),
243 title, path, flags))
246 def save_as(filename, title='Save As...'):
247 """Creates a Save File dialog and returns a filename."""
248 return unicode(QtGui.QFileDialog
249 .getSaveFileName(active_window(), title, filename))
252 def icon(basename):
253 """Given a basename returns a QIcon from the corresponding cola icon."""
254 return QtGui.QIcon(resources.icon(basename))
257 def set_clipboard(text):
258 """Sets the copy/paste buffer to text."""
259 if not text:
260 return
261 clipboard = QtGui.QApplication.instance().clipboard()
262 clipboard.setText(text, QtGui.QClipboard.Clipboard)
263 clipboard.setText(text, QtGui.QClipboard.Selection)
266 def add_action_bool(widget, text, fn, checked, *shortcuts):
267 action = _add_action(widget, text, fn, connect_action_bool, *shortcuts)
268 action.setCheckable(True)
269 action.setChecked(checked)
270 return action
273 def add_action(widget, text, fn, *shortcuts):
274 return _add_action(widget, text, fn, connect_action, *shortcuts)
277 def _add_action(widget, text, fn, connect, *shortcuts):
278 action = QtGui.QAction(text, widget)
279 connect(action, fn)
280 if shortcuts:
281 shortcuts = list(set(shortcuts))
282 action.setShortcuts(shortcuts)
283 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
284 widget.addAction(action)
285 return action
287 def set_selected_item(widget, idx):
288 """Sets a the currently selected item to the item at index idx."""
289 if type(widget) is QtGui.QTreeWidget:
290 item = widget.topLevelItem(idx)
291 if item:
292 widget.setItemSelected(item, True)
293 widget.setCurrentItem(item)
296 def add_items(widget, items):
297 """Adds items to a widget."""
298 for item in items:
299 widget.addItem(item)
302 def set_items(widget, items):
303 """Clear the existing widget contents and set the new items."""
304 widget.clear()
305 add_items(widget, items)
308 def icon_file(filename, staged=False, untracked=False):
309 """Returns a file path representing a corresponding file path."""
310 if staged:
311 if core.exists(filename):
312 ifile = resources.icon('staged-item.png')
313 else:
314 ifile = resources.icon('removed.png')
315 elif untracked:
316 ifile = resources.icon('untracked.png')
317 else:
318 ifile = utils.file_icon(filename)
319 return ifile
322 def icon_for_file(filename, staged=False, untracked=False):
323 """Returns a QIcon for a particular file path."""
324 ifile = icon_file(filename, staged=staged, untracked=untracked)
325 return icon(ifile)
328 def create_treeitem(filename, staged=False, untracked=False, check=True):
329 """Given a filename, return a QListWidgetItem suitable
330 for adding to a QListWidget. "staged" and "untracked"
331 controls whether to use the appropriate icons."""
332 if check:
333 ifile = icon_file(filename, staged=staged, untracked=untracked)
334 else:
335 ifile = resources.icon('staged.png')
336 return create_treewidget_item(filename, ifile)
339 def update_file_icons(widget, items, staged=True,
340 untracked=False, offset=0):
341 """Populate a QListWidget with custom icon items."""
342 for idx, model_item in enumerate(items):
343 item = widget.item(idx+offset)
344 if item:
345 item.setIcon(icon_for_file(model_item, staged, untracked))
347 @memoize
348 def cached_icon(key):
349 """Maintain a cache of standard icons and return cache entries."""
350 style = QtGui.QApplication.instance().style()
351 return style.standardIcon(key)
354 def dir_icon():
355 """Return a standard icon for a directory."""
356 return cached_icon(QtGui.QStyle.SP_DirIcon)
359 def file_icon():
360 """Return a standard icon for a file."""
361 return cached_icon(QtGui.QStyle.SP_FileIcon)
364 def apply_icon():
365 """Return a standard Apply icon"""
366 return cached_icon(QtGui.QStyle.SP_DialogApplyButton)
369 def new_icon():
370 return cached_icon(QtGui.QStyle.SP_FileDialogNewFolder)
373 def save_icon():
374 """Return a standard Save icon"""
375 return cached_icon(QtGui.QStyle.SP_DialogSaveButton)
378 def ok_icon():
379 """Return a standard Ok icon"""
380 return cached_icon(QtGui.QStyle.SP_DialogOkButton)
383 def open_icon():
384 """Return a standard open directory icon"""
385 return cached_icon(QtGui.QStyle.SP_DirOpenIcon)
388 def help_icon():
389 """Return a standard open directory icon"""
390 return cached_icon(QtGui.QStyle.SP_DialogHelpButton)
393 def add_icon():
394 return icon('add.svg')
397 def remove_icon():
398 return icon('remove.svg')
401 def open_file_icon():
402 return icon('open.svg')
405 def options_icon():
406 """Return a standard open directory icon"""
407 return icon('options.svg')
410 def dir_close_icon():
411 """Return a standard closed directory icon"""
412 return cached_icon(QtGui.QStyle.SP_DirClosedIcon)
415 def titlebar_close_icon():
416 """Return a dock widget close icon"""
417 return cached_icon(QtGui.QStyle.SP_TitleBarCloseButton)
420 def titlebar_normal_icon():
421 """Return a dock widget close icon"""
422 return cached_icon(QtGui.QStyle.SP_TitleBarNormalButton)
425 def git_icon():
426 return icon('git.svg')
429 def reload_icon():
430 """Returna standard Refresh icon"""
431 return cached_icon(QtGui.QStyle.SP_BrowserReload)
434 def discard_icon():
435 """Return a standard Discard icon"""
436 return cached_icon(QtGui.QStyle.SP_DialogDiscardButton)
439 def close_icon():
440 """Return a standard Close icon"""
441 return cached_icon(QtGui.QStyle.SP_DialogCloseButton)
444 def add_close_action(widget):
445 """Adds close action and shortcuts to a widget."""
446 return add_action(widget, N_('Close...'),
447 widget.close, QtGui.QKeySequence.Close, 'Ctrl+Q')
450 def center_on_screen(widget):
451 """Move widget to the center of the default screen"""
452 desktop = QtGui.QApplication.instance().desktop()
453 rect = desktop.screenGeometry(QtGui.QCursor().pos())
454 cy = rect.height()/2
455 cx = rect.width()/2
456 widget.move(cx - widget.width()/2, cy - widget.height()/2)
459 def save_state(widget, handler=None):
460 if handler is None:
461 handler = settings.Settings()
462 if gitcfg.instance().get('cola.savewindowsettings', True):
463 handler.save_gui_state(widget)
466 def export_window_state(widget, state, version):
467 # Save the window state
468 windowstate = widget.saveState(version)
469 state['windowstate'] = unicode(windowstate.toBase64().data())
470 return state
473 def apply_window_state(widget, state, version):
474 # Restore the dockwidget, etc. window state
475 try:
476 windowstate = state['windowstate']
477 return widget.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
478 version)
479 except KeyError:
480 return False
483 def apply_state(widget):
484 state = settings.Settings().get_gui_state(widget)
485 return bool(state) and widget.apply_state(state)
488 @memoize
489 def theme_icon(name):
490 """Grab an icon from the current theme with a fallback
492 Support older versions of Qt by catching AttributeError and
493 falling back to our default icons.
496 try:
497 base, ext = os.path.splitext(name)
498 qicon = QtGui.QIcon.fromTheme(base)
499 if not qicon.isNull():
500 return qicon
501 except AttributeError:
502 pass
503 return icon(name)
506 def default_monospace_font():
507 font = QtGui.QFont()
508 family = 'Monospace'
509 if utils.is_darwin():
510 family = 'Monaco'
511 font.setFamily(family)
512 return font
515 def diff_font_str():
516 font_str = gitcfg.instance().get(FONTDIFF)
517 if font_str is None:
518 font = default_monospace_font()
519 font_str = unicode(font.toString())
520 return font_str
523 def diff_font():
524 font_str = diff_font_str()
525 font = QtGui.QFont()
526 font.fromString(font_str)
527 return font
530 def create_button(text='', layout=None, tooltip=None, icon=None):
531 """Create a button, set its title, and add it to the parent."""
532 button = QtGui.QPushButton()
533 button.setCursor(Qt.PointingHandCursor)
534 if text:
535 button.setText(text)
536 if icon:
537 button.setIcon(icon)
538 if tooltip is not None:
539 button.setToolTip(tooltip)
540 if layout is not None:
541 layout.addWidget(button)
542 return button
545 def create_action_button(tooltip, icon):
546 button = QtGui.QPushButton()
547 button.setCursor(Qt.PointingHandCursor)
548 button.setFlat(True)
549 button.setIcon(icon)
550 button.setFixedSize(QtCore.QSize(16, 16))
551 button.setToolTip(tooltip)
552 return button
555 class DockTitleBarWidget(QtGui.QWidget):
557 def __init__(self, parent, title, stretch=True):
558 QtGui.QWidget.__init__(self, parent)
559 self.label = label = QtGui.QLabel()
560 font = label.font()
561 font.setCapitalization(QtGui.QFont.SmallCaps)
562 label.setFont(font)
563 label.setText(title)
565 self.setCursor(Qt.OpenHandCursor)
567 self.close_button = create_action_button(
568 N_('Close'), titlebar_close_icon())
570 self.toggle_button = create_action_button(
571 N_('Detach'), titlebar_normal_icon())
573 self.corner_layout = QtGui.QHBoxLayout()
574 self.corner_layout.setMargin(defs.no_margin)
575 self.corner_layout.setSpacing(defs.spacing)
577 self.main_layout = QtGui.QHBoxLayout()
578 self.main_layout.setMargin(defs.small_margin)
579 self.main_layout.setSpacing(defs.spacing)
580 self.main_layout.addWidget(label)
581 self.main_layout.addSpacing(defs.spacing)
582 if stretch:
583 self.main_layout.addStretch()
584 self.main_layout.addLayout(self.corner_layout)
585 self.main_layout.addSpacing(defs.spacing)
586 self.main_layout.addWidget(self.toggle_button)
587 self.main_layout.addWidget(self.close_button)
589 self.setLayout(self.main_layout)
591 connect_button(self.toggle_button, self.toggle_floating)
592 connect_button(self.close_button, self.toggle_visibility)
594 def toggle_floating(self):
595 self.parent().setFloating(not self.parent().isFloating())
596 self.update_tooltips()
598 def toggle_visibility(self):
599 self.parent().toggleViewAction().trigger()
601 def set_title(self, title):
602 self.label.setText(title)
604 def add_corner_widget(self, widget):
605 self.corner_layout.addWidget(widget)
607 def update_tooltips(self):
608 if self.parent().isFloating():
609 tooltip = N_('Attach')
610 else:
611 tooltip = N_('Detach')
612 self.toggle_button.setToolTip(tooltip)
615 def create_dock(title, parent, stretch=True):
616 """Create a dock widget and set it up accordingly."""
617 dock = QtGui.QDockWidget(parent)
618 dock.setWindowTitle(title)
619 dock.setObjectName(title)
620 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
621 dock.setTitleBarWidget(titlebar)
622 if hasattr(parent, 'dockwidgets'):
623 parent.dockwidgets.append(dock)
624 return dock
627 def create_menu(title, parent):
628 """Create a menu and set its title."""
629 qmenu = QtGui.QMenu(parent)
630 qmenu.setTitle(title)
631 return qmenu
634 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
635 button = QtGui.QToolButton()
636 button.setAutoRaise(True)
637 button.setAutoFillBackground(True)
638 button.setCursor(Qt.PointingHandCursor)
639 if icon is not None:
640 button.setIcon(icon)
641 if text is not None:
642 button.setText(text)
643 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
644 if tooltip is not None:
645 button.setToolTip(tooltip)
646 if layout is not None:
647 layout.addWidget(button)
648 return button
651 # Syntax highlighting
653 def TERMINAL(pattern):
655 Denotes that a pattern is the final pattern that should
656 be matched. If this pattern matches no other formats
657 will be applied, even if they would have matched.
659 return '__TERMINAL__:%s' % pattern
661 # Cache the results of re.compile so that we don't keep
662 # rebuilding the same regexes whenever stylesheets change
663 _RGX_CACHE = {}
665 def rgba(r, g, b, a=255):
666 c = QtGui.QColor()
667 c.setRgb(r, g, b)
668 c.setAlpha(a)
669 return c
671 default_colors = {
672 'color_text': rgba(0x00, 0x00, 0x00),
673 'color_add': rgba(0xcd, 0xff, 0xe0),
674 'color_remove': rgba(0xff, 0xd0, 0xd0),
675 'color_header': rgba(0xbb, 0xbb, 0xbb),
679 class GenericSyntaxHighligher(QtGui.QSyntaxHighlighter):
680 def __init__(self, doc, *args, **kwargs):
681 QtGui.QSyntaxHighlighter.__init__(self, doc)
682 for attr, val in default_colors.items():
683 setattr(self, attr, val)
684 self._rules = []
685 self.enabled = True
686 self.generate_rules()
688 def generate_rules(self):
689 pass
691 def set_enabled(self, enabled):
692 self.enabled = enabled
694 def create_rules(self, *rules):
695 if len(rules) % 2:
696 raise Exception('create_rules requires an even '
697 'number of arguments.')
698 for idx, rule in enumerate(rules):
699 if idx % 2:
700 continue
701 formats = rules[idx+1]
702 terminal = rule.startswith(TERMINAL(''))
703 if terminal:
704 rule = rule[len(TERMINAL('')):]
705 try:
706 regex = _RGX_CACHE[rule]
707 except KeyError:
708 regex = _RGX_CACHE[rule] = re.compile(rule)
709 self._rules.append((regex, formats, terminal,))
711 def formats(self, line):
712 matched = []
713 for regex, fmts, terminal in self._rules:
714 match = regex.match(line)
715 if not match:
716 continue
717 matched.append([match, fmts])
718 if terminal:
719 return matched
720 return matched
722 def mkformat(self, fg=None, bg=None, bold=False):
723 fmt = QtGui.QTextCharFormat()
724 if fg:
725 fmt.setForeground(fg)
726 if bg:
727 fmt.setBackground(bg)
728 if bold:
729 fmt.setFontWeight(QtGui.QFont.Bold)
730 return fmt
732 def highlightBlock(self, qstr):
733 if not self.enabled:
734 return
735 ascii = unicode(qstr)
736 if not ascii:
737 return
738 formats = self.formats(ascii)
739 if not formats:
740 return
741 for match, fmts in formats:
742 start = match.start()
743 groups = match.groups()
745 # No groups in the regex, assume this is a single rule
746 # that spans the entire line
747 if not groups:
748 self.setFormat(0, len(ascii), fmts)
749 continue
751 # Groups exist, rule is a tuple corresponding to group
752 for grpidx, group in enumerate(groups):
753 # allow empty matches
754 if not group:
755 continue
756 # allow None as a no-op format
757 length = len(group)
758 if fmts[grpidx]:
759 self.setFormat(start, start+length,
760 fmts[grpidx])
761 start += length
763 def set_colors(self, colordict):
764 for attr, val in colordict.items():
765 setattr(self, attr, val)
768 class DiffSyntaxHighlighter(GenericSyntaxHighligher):
769 """Implements the diff syntax highlighting
771 This class is used by widgets that display diffs.
774 def __init__(self, doc, whitespace=True):
775 self.whitespace = whitespace
776 GenericSyntaxHighligher.__init__(self, doc)
778 def generate_rules(self):
779 diff_head = self.mkformat(fg=self.color_header)
780 diff_head_bold = self.mkformat(fg=self.color_header, bold=True)
782 diff_add = self.mkformat(fg=self.color_text, bg=self.color_add)
783 diff_remove = self.mkformat(fg=self.color_text, bg=self.color_remove)
785 if self.whitespace:
786 bad_ws = self.mkformat(fg=Qt.black, bg=Qt.red)
788 # We specify the whitespace rule last so that it is
789 # applied after the diff addition/removal rules.
790 # The rules for the header
791 diff_old_rgx = TERMINAL(r'^--- ')
792 diff_new_rgx = TERMINAL(r'^\+\+\+ ')
793 diff_ctx_rgx = TERMINAL(r'^@@ ')
795 diff_hd1_rgx = TERMINAL(r'^diff --git a/.*b/.*')
796 diff_hd2_rgx = TERMINAL(r'^index \S+\.\.\S+')
797 diff_hd3_rgx = TERMINAL(r'^new file mode')
798 diff_hd4_rgx = TERMINAL(r'^deleted file mode')
799 diff_add_rgx = TERMINAL(r'^\+')
800 diff_rmv_rgx = TERMINAL(r'^-')
801 diff_bar_rgx = TERMINAL(r'^([ ]+.*)(\|[ ]+\d+[ ]+[+-]+)$')
802 diff_sts_rgx = (r'(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
803 diff_sum_rgx = (r'(\s+\d+ files changed[^\d]*)'
804 r'(:?\d+ insertions[^\d]*)'
805 r'(:?\d+ deletions.*)$')
807 self.create_rules(diff_old_rgx, diff_head,
808 diff_new_rgx, diff_head,
809 diff_ctx_rgx, diff_head_bold,
810 diff_bar_rgx, (diff_head_bold, diff_head),
811 diff_hd1_rgx, diff_head,
812 diff_hd2_rgx, diff_head,
813 diff_hd3_rgx, diff_head,
814 diff_hd4_rgx, diff_head,
815 diff_add_rgx, diff_add,
816 diff_rmv_rgx, diff_remove,
817 diff_sts_rgx, (None, diff_head,
818 None, diff_head,
819 diff_head),
820 diff_sum_rgx, (diff_head,
821 diff_head,
822 diff_head))
823 if self.whitespace:
824 self.create_rules('(..*?)(\s+)$', (None, bad_ws))
827 def install():
828 Interaction.critical = staticmethod(critical)
829 Interaction.confirm = staticmethod(confirm)
830 Interaction.question = staticmethod(question)
831 Interaction.information = staticmethod(information)