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
.setWindowModality(Qt
.WindowModal
)
85 msgbox
.setWindowTitle(title
)
87 msgbox
.setInformativeText(informative_text
)
88 ok
= msgbox
.addButton(ok_text
, QtGui
.QMessageBox
.ActionRole
)
90 cancel
= msgbox
.addButton(QtGui
.QMessageBox
.Cancel
)
92 msgbox
.setDefaultButton(ok
)
94 msgbox
.setDefaultButton(cancel
)
96 return msgbox
.clickedButton() == ok
99 def critical(title
, message
=None, details
=None):
100 """Show a warning with the provided title and message."""
103 mbox
= QtGui
.QMessageBox(active_window())
104 mbox
.setWindowTitle(title
)
105 mbox
.setTextFormat(QtCore
.Qt
.PlainText
)
106 mbox
.setText(message
)
107 mbox
.setIcon(QtGui
.QMessageBox
.Critical
)
108 mbox
.setStandardButtons(QtGui
.QMessageBox
.Close
)
109 mbox
.setDefaultButton(QtGui
.QMessageBox
.Close
)
111 mbox
.setDetailedText(details
)
115 def information(title
, message
=None, details
=None, informative_text
=None):
116 """Show information with the provided title and message."""
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(QtCore
.Qt
.WindowModal
)
124 mbox
.setTextFormat(QtCore
.Qt
.PlainText
)
125 mbox
.setText(message
)
127 mbox
.setInformativeText(informative_text
)
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
, QtCore
.Qt
.SmoothTransformation
)
134 mbox
.setIconPixmap(pixmap
)
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
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."""
157 item
= tree_widget
.currentItem()
159 id_number
= item
.data(0, QtCore
.Qt
.UserRole
).toInt()[0]
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()
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."""
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
)
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
) ]
191 for item
, widgetitem
in zip(items
[:len(widgetitems
)], widgetitems
):
192 if widgetitem
.isSelected():
193 selected
.append(item
)
198 def selected_item(list_widget
, items
):
199 """Returns the selected item in a QListWidget."""
200 widget_items
= list_widget
.selectedItems()
203 widget_item
= widget_items
[0]
204 row
= list_widget
.row(widget_item
)
211 def selected_items(list_widget
, items
):
212 """Returns the selected item in a QListWidget."""
214 widget_items
= list_widget
.selectedItems()
217 for widget_item
in widget_items
:
218 row
= list_widget
.row(widget_item
)
220 selection
.append(items
[row
])
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(),
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
))
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."""
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
)
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
)
281 shortcuts
= list(set(shortcuts
))
282 action
.setShortcuts(shortcuts
)
283 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
284 widget
.addAction(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
)
292 widget
.setItemSelected(item
, True)
293 widget
.setCurrentItem(item
)
296 def add_items(widget
, items
):
297 """Adds items to a widget."""
302 def set_items(widget
, items
):
303 """Clear the existing widget contents and set the new items."""
305 add_items(widget
, items
)
308 def icon_file(filename
, staged
=False, untracked
=False):
309 """Returns a file path representing a corresponding file path."""
311 if core
.exists(filename
):
312 ifile
= resources
.icon('staged-item.png')
314 ifile
= resources
.icon('removed.png')
316 ifile
= resources
.icon('untracked.png')
318 ifile
= utils
.file_icon(filename
)
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
)
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."""
333 ifile
= icon_file(filename
, staged
=staged
, untracked
=untracked
)
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
)
345 item
.setIcon(icon_for_file(model_item
, staged
, untracked
))
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
)
355 """Return a standard icon for a directory."""
356 return cached_icon(QtGui
.QStyle
.SP_DirIcon
)
360 """Return a standard icon for a file."""
361 return cached_icon(QtGui
.QStyle
.SP_FileIcon
)
365 """Return a standard Apply icon"""
366 return cached_icon(QtGui
.QStyle
.SP_DialogApplyButton
)
370 return cached_icon(QtGui
.QStyle
.SP_FileDialogNewFolder
)
374 """Return a standard Save icon"""
375 return cached_icon(QtGui
.QStyle
.SP_DialogSaveButton
)
379 """Return a standard Ok icon"""
380 return cached_icon(QtGui
.QStyle
.SP_DialogOkButton
)
384 """Return a standard open directory icon"""
385 return cached_icon(QtGui
.QStyle
.SP_DirOpenIcon
)
389 """Return a standard open directory icon"""
390 return cached_icon(QtGui
.QStyle
.SP_DialogHelpButton
)
394 return icon('add.svg')
398 return icon('remove.svg')
401 def open_file_icon():
402 return icon('open.svg')
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
)
426 return icon('git.svg')
430 """Returna standard Refresh icon"""
431 return cached_icon(QtGui
.QStyle
.SP_BrowserReload
)
435 """Return a standard Discard icon"""
436 return cached_icon(QtGui
.QStyle
.SP_DialogDiscardButton
)
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())
456 widget
.move(cx
- widget
.width()/2, cy
- widget
.height()/2)
459 def save_state(widget
, handler
=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())
473 def apply_window_state(widget
, state
, version
):
474 # Restore the dockwidget, etc. window state
476 windowstate
= state
['windowstate']
477 return widget
.restoreState(QtCore
.QByteArray
.fromBase64(str(windowstate
)),
483 def apply_state(widget
):
484 state
= settings
.Settings().get_gui_state(widget
)
485 return bool(state
) and widget
.apply_state(state
)
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.
497 base
, ext
= os
.path
.splitext(name
)
498 qicon
= QtGui
.QIcon
.fromTheme(base
)
499 if not qicon
.isNull():
501 except AttributeError:
506 def default_monospace_font():
509 if utils
.is_darwin():
511 font
.setFamily(family
)
516 font_str
= gitcfg
.instance().get(FONTDIFF
)
518 font
= default_monospace_font()
519 font_str
= unicode(font
.toString())
524 font_str
= diff_font_str()
526 font
.fromString(font_str
)
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
)
538 if tooltip
is not None:
539 button
.setToolTip(tooltip
)
540 if layout
is not None:
541 layout
.addWidget(button
)
545 def create_action_button(tooltip
, icon
):
546 button
= QtGui
.QPushButton()
547 button
.setCursor(QtCore
.Qt
.PointingHandCursor
)
550 button
.setFixedSize(QtCore
.QSize(16, 16))
551 button
.setToolTip(tooltip
)
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()
561 font
.setCapitalization(QtGui
.QFont
.SmallCaps
)
565 self
.setCursor(QtCore
.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
)
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')
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
)
627 def create_menu(title
, parent
):
628 """Create a menu and set its title."""
629 qmenu
= QtGui
.QMenu(parent
)
630 qmenu
.setTitle(title
)
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
)
643 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
645 button
.setToolTip(tooltip
)
646 if layout
is not None:
647 layout
.addWidget(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
665 def rgba(r
, g
, b
, a
=255):
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
)
686 self
.generate_rules()
688 def generate_rules(self
):
691 def set_enabled(self
, enabled
):
692 self
.enabled
= enabled
694 def create_rules(self
, *rules
):
696 raise Exception('create_rules requires an even '
697 'number of arguments.')
698 for idx
, rule
in enumerate(rules
):
701 formats
= rules
[idx
+1]
702 terminal
= rule
.startswith(TERMINAL(''))
704 rule
= rule
[len(TERMINAL('')):]
706 regex
= _RGX_CACHE
[rule
]
708 regex
= _RGX_CACHE
[rule
] = re
.compile(rule
)
709 self
._rules
.append((regex
, formats
, terminal
,))
711 def formats(self
, line
):
713 for regex
, fmts
, terminal
in self
._rules
:
714 match
= regex
.match(line
)
717 matched
.append([match
, fmts
])
722 def mkformat(self
, fg
=None, bg
=None, bold
=False):
723 fmt
= QtGui
.QTextCharFormat()
725 fmt
.setForeground(fg
)
727 fmt
.setBackground(bg
)
729 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
732 def highlightBlock(self
, qstr
):
735 ascii
= unicode(qstr
)
738 formats
= self
.formats(ascii
)
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
748 self
.setFormat(0, len(ascii
), fmts
)
751 # Groups exist, rule is a tuple corresponding to group
752 for grpidx
, group
in enumerate(groups
):
753 # allow empty matches
756 # allow None as a no-op format
759 self
.setFormat(start
, 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
)
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
,
820 diff_sum_rgx
, (diff_head
,
824 self
.create_rules('(..*?)(\s+)$', (None, bad_ws
))
828 Interaction
.critical
= staticmethod(critical
)
829 Interaction
.confirm
= staticmethod(confirm
)
830 Interaction
.question
= staticmethod(question
)
831 Interaction
.information
= staticmethod(information
)