1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
7 from PyQt4
import QtGui
8 from PyQt4
import QtCore
9 from PyQt4
.QtCore
import Qt
10 from PyQt4
.QtCore
import SIGNAL
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
)
42 return QtGui
.QApplication
.activeWindow()
45 def prompt(msg
, title
=None, text
=''):
46 """Presents the user with an input widget and returns the input."""
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
))
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()
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"""
81 elif icon
and isinstance(icon
, basestring
):
82 icon
= QtGui
.QIcon(icon
)
83 msgbox
= QtGui
.QMessageBox(active_window())
84 msgbox
.setWindowTitle(title
)
86 msgbox
.setInformativeText(informative_text
)
87 ok
= msgbox
.addButton(ok_text
, QtGui
.QMessageBox
.ActionRole
)
89 cancel
= msgbox
.addButton(QtGui
.QMessageBox
.Cancel
)
91 msgbox
.setDefaultButton(ok
)
93 msgbox
.setDefaultButton(cancel
)
95 return msgbox
.clickedButton() == ok
98 def critical(title
, message
=None, details
=None):
99 """Show a warning with the provided title and message."""
102 mbox
= QtGui
.QMessageBox(active_window())
103 mbox
.setWindowTitle(title
)
104 mbox
.setTextFormat(QtCore
.Qt
.PlainText
)
105 mbox
.setText(message
)
106 mbox
.setIcon(QtGui
.QMessageBox
.Critical
)
107 mbox
.setStandardButtons(QtGui
.QMessageBox
.Close
)
108 mbox
.setDefaultButton(QtGui
.QMessageBox
.Close
)
110 mbox
.setDetailedText(details
)
114 def information(title
, message
=None, details
=None, informative_text
=None):
115 """Show information with the provided title and message."""
118 mbox
= QtGui
.QMessageBox(active_window())
119 mbox
.setStandardButtons(QtGui
.QMessageBox
.Close
)
120 mbox
.setDefaultButton(QtGui
.QMessageBox
.Close
)
121 mbox
.setWindowTitle(title
)
122 mbox
.setWindowModality(QtCore
.Qt
.WindowModal
)
123 mbox
.setTextFormat(QtCore
.Qt
.PlainText
)
124 mbox
.setText(message
)
126 mbox
.setInformativeText(informative_text
)
128 mbox
.setDetailedText(details
)
129 # Render git.svg into a 1-inch wide pixmap
130 pixmap
= QtGui
.QPixmap(resources
.icon('git.svg'))
131 xres
= pixmap
.physicalDpiX()
132 pixmap
= pixmap
.scaledToHeight(xres
, QtCore
.Qt
.SmoothTransformation
)
133 mbox
.setIconPixmap(pixmap
)
137 def question(title
, msg
, default
=True):
138 """Launches a QMessageBox question with the provided title and message.
139 Passing "default=False" will make "No" the default choice."""
140 yes
= QtGui
.QMessageBox
.Yes
141 no
= QtGui
.QMessageBox
.No
147 result
= (QtGui
.QMessageBox
148 .question(active_window(), title
, msg
, buttons
, default
))
149 return result
== QtGui
.QMessageBox
.Yes
152 def selected_treeitem(tree_widget
):
153 """Returns a(id_number, is_selected) for a QTreeWidget."""
156 item
= tree_widget
.currentItem()
158 id_number
= item
.data(0, QtCore
.Qt
.UserRole
).toInt()[0]
160 return(id_number
, selected
)
163 def selected_row(list_widget
):
164 """Returns a(row_number, is_selected) tuple for a QListWidget."""
165 items
= list_widget
.selectedItems()
169 return (list_widget
.row(item
), True)
172 def selection_list(listwidget
, items
):
173 """Returns an array of model items that correspond to
174 the selected QListWidget indices."""
176 itemcount
= listwidget
.count()
177 widgetitems
= [ listwidget
.item(idx
) for idx
in range(itemcount
) ]
179 for item
, widgetitem
in zip(items
, widgetitems
):
180 if widgetitem
.isSelected():
181 selected
.append(item
)
185 def tree_selection(treeitem
, items
):
186 """Returns model items that correspond to selected widget indices"""
187 itemcount
= treeitem
.childCount()
188 widgetitems
= [ treeitem
.child(idx
) for idx
in range(itemcount
) ]
190 for item
, widgetitem
in zip(items
[:len(widgetitems
)], widgetitems
):
191 if widgetitem
.isSelected():
192 selected
.append(item
)
197 def selected_item(list_widget
, items
):
198 """Returns the selected item in a QListWidget."""
199 widget_items
= list_widget
.selectedItems()
202 widget_item
= widget_items
[0]
203 row
= list_widget
.row(widget_item
)
210 def selected_items(list_widget
, items
):
211 """Returns the selected item in a QListWidget."""
213 widget_items
= list_widget
.selectedItems()
216 for widget_item
in widget_items
:
217 row
= list_widget
.row(widget_item
)
219 selection
.append(items
[row
])
223 def open_dialog(title
, filename
=None):
224 """Creates an Open File dialog and returns a filename."""
225 return unicode(QtGui
.QFileDialog
226 .getOpenFileName(active_window(), title
, filename
))
229 def opendir_dialog(title
, path
):
230 """Prompts for a directory path"""
232 flags
= (QtGui
.QFileDialog
.ShowDirsOnly |
233 QtGui
.QFileDialog
.DontResolveSymlinks
)
234 return unicode(QtGui
.QFileDialog
235 .getExistingDirectory(active_window(),
239 def save_as(filename
, title
='Save As...'):
240 """Creates a Save File dialog and returns a filename."""
241 return unicode(QtGui
.QFileDialog
242 .getSaveFileName(active_window(), title
, filename
))
246 """Given a basename returns a QIcon from the corresponding cola icon."""
247 return QtGui
.QIcon(resources
.icon(basename
))
250 def set_clipboard(text
):
251 """Sets the copy/paste buffer to text."""
254 clipboard
= QtGui
.QApplication
.instance().clipboard()
255 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
256 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
259 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
260 action
= _add_action(widget
, text
, fn
, connect_action_bool
, *shortcuts
)
261 action
.setCheckable(True)
262 action
.setChecked(checked
)
266 def add_action(widget
, text
, fn
, *shortcuts
):
267 return _add_action(widget
, text
, fn
, connect_action
, *shortcuts
)
270 def _add_action(widget
, text
, fn
, connect
, *shortcuts
):
271 action
= QtGui
.QAction(text
, widget
)
274 shortcuts
= list(set(shortcuts
))
275 action
.setShortcuts(shortcuts
)
276 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
277 widget
.addAction(action
)
280 def set_selected_item(widget
, idx
):
281 """Sets a the currently selected item to the item at index idx."""
282 if type(widget
) is QtGui
.QTreeWidget
:
283 item
= widget
.topLevelItem(idx
)
285 widget
.setItemSelected(item
, True)
286 widget
.setCurrentItem(item
)
289 def add_items(widget
, items
):
290 """Adds items to a widget."""
295 def set_items(widget
, items
):
296 """Clear the existing widget contents and set the new items."""
298 add_items(widget
, items
)
301 def icon_file(filename
, staged
=False, untracked
=False):
302 """Returns a file path representing a corresponding file path."""
304 if core
.exists(filename
):
305 ifile
= resources
.icon('staged-item.png')
307 ifile
= resources
.icon('removed.png')
309 ifile
= resources
.icon('untracked.png')
311 ifile
= utils
.file_icon(filename
)
315 def icon_for_file(filename
, staged
=False, untracked
=False):
316 """Returns a QIcon for a particular file path."""
317 ifile
= icon_file(filename
, staged
=staged
, untracked
=untracked
)
321 def create_treeitem(filename
, staged
=False, untracked
=False, check
=True):
322 """Given a filename, return a QListWidgetItem suitable
323 for adding to a QListWidget. "staged" and "untracked"
324 controls whether to use the appropriate icons."""
326 ifile
= icon_file(filename
, staged
=staged
, untracked
=untracked
)
328 ifile
= resources
.icon('staged.png')
329 return create_treewidget_item(filename
, ifile
)
332 def update_file_icons(widget
, items
, staged
=True,
333 untracked
=False, offset
=0):
334 """Populate a QListWidget with custom icon items."""
335 for idx
, model_item
in enumerate(items
):
336 item
= widget
.item(idx
+offset
)
338 item
.setIcon(icon_for_file(model_item
, staged
, untracked
))
341 def cached_icon(key
):
342 """Maintain a cache of standard icons and return cache entries."""
343 style
= QtGui
.QApplication
.instance().style()
344 return style
.standardIcon(key
)
348 """Return a standard icon for a directory."""
349 return cached_icon(QtGui
.QStyle
.SP_DirIcon
)
353 """Return a standard icon for a file."""
354 return cached_icon(QtGui
.QStyle
.SP_FileIcon
)
358 """Return a standard Apply icon"""
359 return cached_icon(QtGui
.QStyle
.SP_DialogApplyButton
)
363 return cached_icon(QtGui
.QStyle
.SP_FileDialogNewFolder
)
367 """Return a standard Save icon"""
368 return cached_icon(QtGui
.QStyle
.SP_DialogSaveButton
)
372 """Return a standard Ok icon"""
373 return cached_icon(QtGui
.QStyle
.SP_DialogOkButton
)
377 """Return a standard open directory icon"""
378 return cached_icon(QtGui
.QStyle
.SP_DirOpenIcon
)
382 """Return a standard open directory icon"""
383 return cached_icon(QtGui
.QStyle
.SP_DialogHelpButton
)
386 def open_file_icon():
387 return icon('open.svg')
391 """Return a standard open directory icon"""
392 return icon('options.svg')
395 def dir_close_icon():
396 """Return a standard closed directory icon"""
397 return cached_icon(QtGui
.QStyle
.SP_DirClosedIcon
)
400 def titlebar_close_icon():
401 """Return a dock widget close icon"""
402 return cached_icon(QtGui
.QStyle
.SP_TitleBarCloseButton
)
405 def titlebar_normal_icon():
406 """Return a dock widget close icon"""
407 return cached_icon(QtGui
.QStyle
.SP_TitleBarNormalButton
)
411 return icon('git.svg')
415 """Returna standard Refresh icon"""
416 return cached_icon(QtGui
.QStyle
.SP_BrowserReload
)
420 """Return a standard Discard icon"""
421 return cached_icon(QtGui
.QStyle
.SP_DialogDiscardButton
)
425 """Return a standard Close icon"""
426 return cached_icon(QtGui
.QStyle
.SP_DialogCloseButton
)
429 def add_close_action(widget
):
430 """Adds close action and shortcuts to a widget."""
431 return add_action(widget
, N_('Close...'),
432 widget
.close
, QtGui
.QKeySequence
.Close
, 'Ctrl+Q')
435 def center_on_screen(widget
):
436 """Move widget to the center of the default screen"""
437 desktop
= QtGui
.QApplication
.instance().desktop()
438 rect
= desktop
.screenGeometry(QtGui
.QCursor().pos())
441 widget
.move(cx
- widget
.width()/2, cy
- widget
.height()/2)
444 def save_state(widget
, handler
=None):
446 handler
= settings
.Settings()
447 if gitcfg
.instance().get('cola.savewindowsettings', True):
448 handler
.save_gui_state(widget
)
451 def export_window_state(widget
, state
, version
):
452 # Save the window state
453 windowstate
= widget
.saveState(version
)
454 state
['windowstate'] = unicode(windowstate
.toBase64().data())
458 def apply_window_state(widget
, state
, version
):
459 # Restore the dockwidget, etc. window state
461 windowstate
= state
['windowstate']
462 return widget
.restoreState(QtCore
.QByteArray
.fromBase64(str(windowstate
)),
468 def apply_state(widget
):
469 state
= settings
.Settings().get_gui_state(widget
)
470 return bool(state
) and widget
.apply_state(state
)
474 def theme_icon(name
):
475 """Grab an icon from the current theme with a fallback
477 Support older versions of Qt by catching AttributeError and
478 falling back to our default icons.
482 base
, ext
= os
.path
.splitext(name
)
483 qicon
= QtGui
.QIcon
.fromTheme(base
)
484 if not qicon
.isNull():
486 except AttributeError:
491 def default_monospace_font():
494 if utils
.is_darwin():
496 font
.setFamily(family
)
501 font_str
= gitcfg
.instance().get(FONTDIFF
)
503 font
= default_monospace_font()
504 font_str
= unicode(font
.toString())
509 font_str
= diff_font_str()
511 font
.fromString(font_str
)
515 def create_button(text
='', layout
=None, tooltip
=None, icon
=None):
516 """Create a button, set its title, and add it to the parent."""
517 button
= QtGui
.QPushButton()
518 button
.setCursor(Qt
.PointingHandCursor
)
523 if tooltip
is not None:
524 button
.setToolTip(tooltip
)
525 if layout
is not None:
526 layout
.addWidget(button
)
530 def create_action_button(tooltip
, icon
):
531 button
= QtGui
.QPushButton()
532 button
.setCursor(QtCore
.Qt
.PointingHandCursor
)
535 button
.setFixedSize(QtCore
.QSize(16, 16))
536 button
.setToolTip(tooltip
)
540 class DockTitleBarWidget(QtGui
.QWidget
):
542 def __init__(self
, parent
, title
, stretch
=True):
543 QtGui
.QWidget
.__init
__(self
, parent
)
544 self
.label
= label
= QtGui
.QLabel()
546 font
.setCapitalization(QtGui
.QFont
.SmallCaps
)
550 self
.setCursor(QtCore
.Qt
.OpenHandCursor
)
552 self
.close_button
= create_action_button(
553 N_('Close'), titlebar_close_icon())
555 self
.toggle_button
= create_action_button(
556 N_('Detach'), titlebar_normal_icon())
558 self
.corner_layout
= QtGui
.QHBoxLayout()
559 self
.corner_layout
.setMargin(defs
.no_margin
)
560 self
.corner_layout
.setSpacing(defs
.spacing
)
562 self
.main_layout
= QtGui
.QHBoxLayout()
563 self
.main_layout
.setMargin(defs
.small_margin
)
564 self
.main_layout
.setSpacing(defs
.spacing
)
565 self
.main_layout
.addWidget(label
)
566 self
.main_layout
.addSpacing(defs
.spacing
)
568 self
.main_layout
.addStretch()
569 self
.main_layout
.addLayout(self
.corner_layout
)
570 self
.main_layout
.addSpacing(defs
.spacing
)
571 self
.main_layout
.addWidget(self
.toggle_button
)
572 self
.main_layout
.addWidget(self
.close_button
)
574 self
.setLayout(self
.main_layout
)
576 connect_button(self
.toggle_button
, self
.toggle_floating
)
577 connect_button(self
.close_button
, self
.toggle_visibility
)
579 def toggle_floating(self
):
580 self
.parent().setFloating(not self
.parent().isFloating())
581 self
.update_tooltips()
583 def toggle_visibility(self
):
584 self
.parent().toggleViewAction().trigger()
586 def set_title(self
, title
):
587 self
.label
.setText(title
)
589 def add_corner_widget(self
, widget
):
590 self
.corner_layout
.addWidget(widget
)
592 def update_tooltips(self
):
593 if self
.parent().isFloating():
594 tooltip
= N_('Attach')
596 tooltip
= N_('Detach')
597 self
.toggle_button
.setToolTip(tooltip
)
600 def create_dock(title
, parent
, stretch
=True):
601 """Create a dock widget and set it up accordingly."""
602 dock
= QtGui
.QDockWidget(parent
)
603 dock
.setWindowTitle(title
)
604 dock
.setObjectName(title
)
605 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
606 dock
.setTitleBarWidget(titlebar
)
607 if hasattr(parent
, 'dockwidgets'):
608 parent
.dockwidgets
.append(dock
)
612 def create_menu(title
, parent
):
613 """Create a menu and set its title."""
614 qmenu
= QtGui
.QMenu(parent
)
615 qmenu
.setTitle(title
)
619 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
620 button
= QtGui
.QToolButton()
621 button
.setAutoRaise(True)
622 button
.setAutoFillBackground(True)
623 button
.setCursor(Qt
.PointingHandCursor
)
628 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
630 button
.setToolTip(tooltip
)
631 if layout
is not None:
632 layout
.addWidget(button
)
636 # Syntax highlighting
638 def TERMINAL(pattern
):
640 Denotes that a pattern is the final pattern that should
641 be matched. If this pattern matches no other formats
642 will be applied, even if they would have matched.
644 return '__TERMINAL__:%s' % pattern
646 # Cache the results of re.compile so that we don't keep
647 # rebuilding the same regexes whenever stylesheets change
650 def rgba(r
, g
, b
, a
=255):
657 'color_text': rgba(0x00, 0x00, 0x00),
658 'color_add': rgba(0xcd, 0xff, 0xe0),
659 'color_remove': rgba(0xff, 0xd0, 0xd0),
660 'color_header': rgba(0xbb, 0xbb, 0xbb),
664 class GenericSyntaxHighligher(QtGui
.QSyntaxHighlighter
):
665 def __init__(self
, doc
, *args
, **kwargs
):
666 QtGui
.QSyntaxHighlighter
.__init
__(self
, doc
)
667 for attr
, val
in default_colors
.items():
668 setattr(self
, attr
, val
)
671 self
.generate_rules()
673 def generate_rules(self
):
676 def set_enabled(self
, enabled
):
677 self
.enabled
= enabled
679 def create_rules(self
, *rules
):
681 raise Exception('create_rules requires an even '
682 'number of arguments.')
683 for idx
, rule
in enumerate(rules
):
686 formats
= rules
[idx
+1]
687 terminal
= rule
.startswith(TERMINAL(''))
689 rule
= rule
[len(TERMINAL('')):]
691 regex
= _RGX_CACHE
[rule
]
693 regex
= _RGX_CACHE
[rule
] = re
.compile(rule
)
694 self
._rules
.append((regex
, formats
, terminal
,))
696 def formats(self
, line
):
698 for regex
, fmts
, terminal
in self
._rules
:
699 match
= regex
.match(line
)
702 matched
.append([match
, fmts
])
707 def mkformat(self
, fg
=None, bg
=None, bold
=False):
708 fmt
= QtGui
.QTextCharFormat()
710 fmt
.setForeground(fg
)
712 fmt
.setBackground(bg
)
714 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
717 def highlightBlock(self
, qstr
):
720 ascii
= unicode(qstr
)
723 formats
= self
.formats(ascii
)
726 for match
, fmts
in formats
:
727 start
= match
.start()
728 groups
= match
.groups()
730 # No groups in the regex, assume this is a single rule
731 # that spans the entire line
733 self
.setFormat(0, len(ascii
), fmts
)
736 # Groups exist, rule is a tuple corresponding to group
737 for grpidx
, group
in enumerate(groups
):
738 # allow empty matches
741 # allow None as a no-op format
744 self
.setFormat(start
, start
+length
,
748 def set_colors(self
, colordict
):
749 for attr
, val
in colordict
.items():
750 setattr(self
, attr
, val
)
753 class DiffSyntaxHighlighter(GenericSyntaxHighligher
):
754 """Implements the diff syntax highlighting
756 This class is used by widgets that display diffs.
759 def __init__(self
, doc
, whitespace
=True):
760 self
.whitespace
= whitespace
761 GenericSyntaxHighligher
.__init
__(self
, doc
)
763 def generate_rules(self
):
764 diff_head
= self
.mkformat(fg
=self
.color_header
)
765 diff_head_bold
= self
.mkformat(fg
=self
.color_header
, bold
=True)
767 diff_add
= self
.mkformat(fg
=self
.color_text
, bg
=self
.color_add
)
768 diff_remove
= self
.mkformat(fg
=self
.color_text
, bg
=self
.color_remove
)
771 bad_ws
= self
.mkformat(fg
=Qt
.black
, bg
=Qt
.red
)
773 # We specify the whitespace rule last so that it is
774 # applied after the diff addition/removal rules.
775 # The rules for the header
776 diff_old_rgx
= TERMINAL(r
'^--- ')
777 diff_new_rgx
= TERMINAL(r
'^\+\+\+ ')
778 diff_ctx_rgx
= TERMINAL(r
'^@@ ')
780 diff_hd1_rgx
= TERMINAL(r
'^diff --git a/.*b/.*')
781 diff_hd2_rgx
= TERMINAL(r
'^index \S+\.\.\S+')
782 diff_hd3_rgx
= TERMINAL(r
'^new file mode')
783 diff_hd4_rgx
= TERMINAL(r
'^deleted file mode')
784 diff_add_rgx
= TERMINAL(r
'^\+')
785 diff_rmv_rgx
= TERMINAL(r
'^-')
786 diff_bar_rgx
= TERMINAL(r
'^([ ]+.*)(\|[ ]+\d+[ ]+[+-]+)$')
787 diff_sts_rgx
= (r
'(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
788 diff_sum_rgx
= (r
'(\s+\d+ files changed[^\d]*)'
789 r
'(:?\d+ insertions[^\d]*)'
790 r
'(:?\d+ deletions.*)$')
792 self
.create_rules(diff_old_rgx
, diff_head
,
793 diff_new_rgx
, diff_head
,
794 diff_ctx_rgx
, diff_head_bold
,
795 diff_bar_rgx
, (diff_head_bold
, diff_head
),
796 diff_hd1_rgx
, diff_head
,
797 diff_hd2_rgx
, diff_head
,
798 diff_hd3_rgx
, diff_head
,
799 diff_hd4_rgx
, diff_head
,
800 diff_add_rgx
, diff_add
,
801 diff_rmv_rgx
, diff_remove
,
802 diff_sts_rgx
, (None, diff_head
,
805 diff_sum_rgx
, (diff_head
,
809 self
.create_rules('(..*?)(\s+)$', (None, bad_ws
))
813 Interaction
.critical
= staticmethod(critical
)
814 Interaction
.confirm
= staticmethod(confirm
)
815 Interaction
.question
= staticmethod(question
)
816 Interaction
.information
= staticmethod(information
)