1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
4 from __future__
import division
, absolute_import
, unicode_literals
10 from PyQt4
import QtGui
11 from PyQt4
import QtCore
12 from PyQt4
.QtCore
import Qt
13 from PyQt4
.QtCore
import SIGNAL
16 from cola
import gitcfg
17 from cola
import utils
18 from cola
import resources
19 from cola
.decorators
import memoize
20 from cola
.i18n
import N_
21 from cola
.interaction
import Interaction
22 from cola
.models
.prefs
import FONTDIFF
23 from cola
.widgets
import defs
24 from cola
.compat
import ustr
27 KNOWN_FILE_MIME_TYPES
= [
28 ('text', 'script.png'),
29 ('image', 'image.png'),
30 ('python', 'script.png'),
31 ('ruby', 'script.png'),
32 ('shell', 'script.png'),
33 ('perl', 'script.png'),
34 ('octet', 'binary.png'),
37 KNOWN_FILE_EXTENSIONS
= {
38 '.java': 'script.png',
39 '.groovy': 'script.png',
47 def connect_action(action
, fn
):
48 """Connectc an action to a function"""
49 action
.connect(action
, SIGNAL('triggered()'), fn
)
52 def connect_action_bool(action
, fn
):
53 """Connect a triggered(bool) action to a function"""
54 action
.connect(action
, SIGNAL('triggered(bool)'), fn
)
57 def connect_button(button
, fn
):
58 """Connect a button to a function"""
59 button
.connect(button
, SIGNAL('clicked()'), fn
)
62 def button_action(button
, action
):
63 """Make a button trigger an action"""
64 connect_button(button
, action
.trigger
)
67 def connect_toggle(toggle
, fn
):
68 toggle
.connect(toggle
, SIGNAL('toggled(bool)'), fn
)
72 return QtGui
.QApplication
.activeWindow()
75 def hbox(margin
, spacing
, *items
):
76 return box(QtGui
.QHBoxLayout
, margin
, spacing
, *items
)
79 def vbox(margin
, spacing
, *items
):
80 return box(QtGui
.QVBoxLayout
, margin
, spacing
, *items
)
87 def box(cls
, margin
, spacing
, *items
):
91 layout
.setMargin(margin
)
92 layout
.setSpacing(spacing
)
99 elif isinstance(i
, QtGui
.QWidget
):
101 elif isinstance(i
, (QtGui
.QHBoxLayout
, QtGui
.QVBoxLayout
,
102 QtGui
.QFormLayout
, QtGui
.QLayout
)):
104 elif isinstance(i
, (int, long)):
110 def form(margin
, spacing
, *widgets
):
111 layout
= QtGui
.QFormLayout()
112 layout
.setMargin(margin
)
113 layout
.setSpacing(spacing
)
114 layout
.setFieldGrowthPolicy(QtGui
.QFormLayout
.ExpandingFieldsGrow
)
116 for idx
, (label
, widget
) in enumerate(widgets
):
117 if isinstance(label
, (str, ustr
)):
118 layout
.addRow(label
, widget
)
120 layout
.setWidget(idx
, QtGui
.QFormLayout
.LabelRole
, label
)
121 layout
.setWidget(idx
, QtGui
.QFormLayout
.FieldRole
, widget
)
126 def grid(margin
, spacing
, *widgets
):
127 layout
= QtGui
.QGridLayout()
128 layout
.setMargin(defs
.no_margin
)
129 layout
.setSpacing(defs
.spacing
)
133 if isinstance(item
, QtGui
.QWidget
):
134 layout
.addWidget(*row
)
135 elif isinstance(item
, QtGui
.QLayoutItem
):
141 def splitter(orientation
, *widgets
):
142 layout
= QtGui
.QSplitter()
143 layout
.setOrientation(orientation
)
144 layout
.setHandleWidth(defs
.handle_width
)
145 layout
.setChildrenCollapsible(True)
146 for idx
, widget
in enumerate(widgets
):
147 layout
.addWidget(widget
)
148 layout
.setStretchFactor(idx
, 1)
152 def prompt(msg
, title
=None, text
=''):
153 """Presents the user with an input widget and returns the input."""
156 result
= QtGui
.QInputDialog
.getText(active_window(), msg
, title
,
157 QtGui
.QLineEdit
.Normal
, text
)
158 return (ustr(result
[0]), result
[1])
161 def create_listwidget_item(text
, filename
):
162 """Creates a QListWidgetItem with text and the icon at filename."""
163 item
= QtGui
.QListWidgetItem()
164 item
.setIcon(QtGui
.QIcon(filename
))
169 class TreeWidgetItem(QtGui
.QTreeWidgetItem
):
171 TYPE
= QtGui
.QStandardItem
.UserType
+ 101
173 def __init__(self
, path
, icon
, deleted
):
174 QtGui
.QTreeWidgetItem
.__init
__(self
)
176 self
.deleted
= deleted
177 self
.setIcon(0, cached_icon_from_path(icon
))
178 self
.setText(0, path
)
184 def paths_from_indexes(model
, indexes
,
185 item_type
=TreeWidgetItem
.TYPE
,
187 """Return paths from a list of QStandardItemModel indexes"""
188 items
= [model
.itemFromIndex(i
) for i
in indexes
]
189 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
192 def paths_from_items(items
,
193 item_type
=TreeWidgetItem
.TYPE
,
195 """Return a list of paths from a list of items"""
196 if item_filter
is None:
197 item_filter
= lambda x
: True
198 return [i
.path
for i
in items
199 if i
.type() == item_type
and item_filter(i
)]
203 def cached_icon_from_path(filename
):
204 return QtGui
.QIcon(filename
)
207 def mkicon(icon
, default
=None):
208 if icon
is None and default
is not None:
210 elif icon
and isinstance(icon
, (str, ustr
)):
211 icon
= QtGui
.QIcon(icon
)
215 def confirm(title
, text
, informative_text
, ok_text
,
216 icon
=None, default
=True,
217 cancel_text
=None, cancel_icon
=None):
218 """Confirm that an action should take place"""
219 msgbox
= QtGui
.QMessageBox(active_window())
220 msgbox
.setWindowModality(Qt
.WindowModal
)
221 msgbox
.setWindowTitle(title
)
223 msgbox
.setInformativeText(informative_text
)
225 icon
= mkicon(icon
, ok_icon
)
226 ok
= msgbox
.addButton(ok_text
, QtGui
.QMessageBox
.ActionRole
)
229 cancel
= msgbox
.addButton(QtGui
.QMessageBox
.Cancel
)
230 cancel_icon
= mkicon(cancel_icon
, discard_icon
)
231 cancel
.setIcon(cancel_icon
)
233 cancel
.setText(cancel_text
)
236 msgbox
.setDefaultButton(ok
)
238 msgbox
.setDefaultButton(cancel
)
240 return msgbox
.clickedButton() == ok
243 class ResizeableMessageBox(QtGui
.QMessageBox
):
245 def __init__(self
, parent
):
246 QtGui
.QMessageBox
.__init
__(self
, parent
)
247 self
.setMouseTracking(True)
248 self
.setSizeGripEnabled(True)
250 def event(self
, event
):
251 res
= QtGui
.QMessageBox
.event(self
, event
)
252 event_type
= event
.type()
253 if (event_type
== QtCore
.QEvent
.MouseMove
or
254 event_type
== QtCore
.QEvent
.MouseButtonPress
):
255 maxi
= QtCore
.QSize(1024*4, 1024*4)
256 self
.setMaximumSize(maxi
)
257 text
= self
.findChild(QtGui
.QTextEdit
)
259 expand
= QtGui
.QSizePolicy
.Expanding
260 text
.setSizePolicy(QtGui
.QSizePolicy(expand
, expand
))
261 text
.setMaximumSize(maxi
)
265 def critical(title
, message
=None, details
=None):
266 """Show a warning with the provided title and message."""
269 mbox
= ResizeableMessageBox(active_window())
270 mbox
.setWindowTitle(title
)
271 mbox
.setTextFormat(Qt
.PlainText
)
272 mbox
.setText(message
)
273 mbox
.setIcon(QtGui
.QMessageBox
.Critical
)
274 mbox
.setStandardButtons(QtGui
.QMessageBox
.Close
)
275 mbox
.setDefaultButton(QtGui
.QMessageBox
.Close
)
277 mbox
.setDetailedText(details
)
281 def information(title
, message
=None, details
=None, informative_text
=None):
282 """Show information with the provided title and message."""
285 mbox
= QtGui
.QMessageBox(active_window())
286 mbox
.setStandardButtons(QtGui
.QMessageBox
.Close
)
287 mbox
.setDefaultButton(QtGui
.QMessageBox
.Close
)
288 mbox
.setWindowTitle(title
)
289 mbox
.setWindowModality(Qt
.WindowModal
)
290 mbox
.setTextFormat(Qt
.PlainText
)
291 mbox
.setText(message
)
293 mbox
.setInformativeText(informative_text
)
295 mbox
.setDetailedText(details
)
296 # Render git-cola.svg into a 1-inch wide pixmap
297 pixmap
= git_icon().pixmap(96)
298 mbox
.setIconPixmap(pixmap
)
302 def question(title
, msg
, default
=True):
303 """Launches a QMessageBox question with the provided title and message.
304 Passing "default=False" will make "No" the default choice."""
305 yes
= QtGui
.QMessageBox
.Yes
306 no
= QtGui
.QMessageBox
.No
312 result
= (QtGui
.QMessageBox
313 .question(active_window(), title
, msg
, buttons
, default
))
314 return result
== QtGui
.QMessageBox
.Yes
317 def tree_selection(tree_item
, items
):
318 """Returns an array of model items that correspond to the selected
319 QTreeWidgetItem children"""
321 count
= min(tree_item
.childCount(), len(items
))
322 for idx
in range(count
):
323 if tree_item
.child(idx
).isSelected():
324 selected
.append(items
[idx
])
329 def tree_selection_items(tree_item
):
330 """Returns selected widget items"""
332 for idx
in range(tree_item
.childCount()):
333 child
= tree_item
.child(idx
)
334 if child
.isSelected():
335 selected
.append(child
)
340 def selected_item(list_widget
, items
):
341 """Returns the model item that corresponds to the selected QListWidget
343 widget_items
= list_widget
.selectedItems()
346 widget_item
= widget_items
[0]
347 row
= list_widget
.row(widget_item
)
354 def selected_items(list_widget
, items
):
355 """Returns an array of model items that correspond to the selected
357 item_count
= len(items
)
359 for widget_item
in list_widget
.selectedItems():
360 row
= list_widget
.row(widget_item
)
362 selected
.append(items
[row
])
366 def open_file(title
, directory
=None):
367 """Creates an Open File dialog and returns a filename."""
368 return ustr(QtGui
.QFileDialog
369 .getOpenFileName(active_window(), title
, directory
))
372 def open_files(title
, directory
=None, filter=None):
373 """Creates an Open File dialog and returns a list of filenames."""
374 return (QtGui
.QFileDialog
375 .getOpenFileNames(active_window(), title
, directory
, filter))
378 def opendir_dialog(title
, path
):
379 """Prompts for a directory path"""
381 flags
= (QtGui
.QFileDialog
.ShowDirsOnly |
382 QtGui
.QFileDialog
.DontResolveSymlinks
)
383 return ustr(QtGui
.QFileDialog
384 .getExistingDirectory(active_window(),
388 def save_as(filename
, title
='Save As...'):
389 """Creates a Save File dialog and returns a filename."""
390 return ustr(QtGui
.QFileDialog
391 .getSaveFileName(active_window(), title
, filename
))
395 """Given a basename returns a QIcon from the corresponding cola icon."""
396 return QtGui
.QIcon(resources
.icon(basename
))
399 def copy_path(filename
, absolute
=True):
400 """Copy a filename to the clipboard"""
404 filename
= core
.abspath(filename
)
405 set_clipboard(filename
)
408 def set_clipboard(text
):
409 """Sets the copy/paste buffer to text."""
412 clipboard
= QtGui
.QApplication
.instance().clipboard()
413 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
414 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
417 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
418 action
= _add_action(widget
, text
, fn
, connect_action_bool
, *shortcuts
)
419 action
.setCheckable(True)
420 action
.setChecked(checked
)
424 def add_action(widget
, text
, fn
, *shortcuts
):
425 return _add_action(widget
, text
, fn
, connect_action
, *shortcuts
)
428 def _add_action(widget
, text
, fn
, connect
, *shortcuts
):
429 action
= QtGui
.QAction(text
, widget
)
432 action
.setShortcuts(shortcuts
)
433 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
434 widget
.addAction(action
)
438 def set_selected_item(widget
, idx
):
439 """Sets a the currently selected item to the item at index idx."""
440 if type(widget
) is QtGui
.QTreeWidget
:
441 item
= widget
.topLevelItem(idx
)
443 widget
.setItemSelected(item
, True)
444 widget
.setCurrentItem(item
)
447 def add_items(widget
, items
):
448 """Adds items to a widget."""
453 def set_items(widget
, items
):
454 """Clear the existing widget contents and set the new items."""
456 add_items(widget
, items
)
459 def icon_name_for_filename(filename
):
460 """Returns an icon name based on the filename."""
461 mimetype
= mimetypes
.guess_type(filename
)[0]
462 if mimetype
is not None:
463 mimetype
= mimetype
.lower()
464 for filetype
, icon_name
in KNOWN_FILE_MIME_TYPES
:
465 if filetype
in mimetype
:
467 extension
= os
.path
.splitext(filename
)[1]
468 return KNOWN_FILE_EXTENSIONS
.get(extension
.lower(), 'generic.png')
471 def icon_from_filename(filename
):
472 icon_name
= icon_name_for_filename(filename
)
473 return cached_icon_from_path(resources
.icon(icon_name
))
476 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
477 """Given a filename, return a TreeListItem suitable for adding to a
478 QListWidget. "staged", "deleted, and "untracked" control whether to use
479 the appropriate icons."""
481 icon_name
= 'removed.png'
483 icon_name
= 'staged-item.png'
485 icon_name
= 'untracked.png'
487 icon_name
= icon_name_for_filename(filename
)
488 return TreeWidgetItem(filename
, resources
.icon(icon_name
), deleted
=deleted
)
492 def cached_icon(key
):
493 """Maintain a cache of standard icons and return cache entries."""
494 style
= QtGui
.QApplication
.instance().style()
495 return style
.standardIcon(key
)
499 """Return a standard icon for a directory."""
500 return cached_icon(QtGui
.QStyle
.SP_DirIcon
)
504 """Return a standard icon for a file."""
505 return cached_icon(QtGui
.QStyle
.SP_FileIcon
)
509 """Return a standard Apply icon"""
510 return cached_icon(QtGui
.QStyle
.SP_DialogApplyButton
)
514 return cached_icon(QtGui
.QStyle
.SP_FileDialogNewFolder
)
518 """Return a standard Save icon"""
519 return cached_icon(QtGui
.QStyle
.SP_DialogSaveButton
)
523 """Return a standard Ok icon"""
524 return cached_icon(QtGui
.QStyle
.SP_DialogOkButton
)
528 """Return a standard open directory icon"""
529 return cached_icon(QtGui
.QStyle
.SP_DirOpenIcon
)
533 """Return a standard open directory icon"""
534 return cached_icon(QtGui
.QStyle
.SP_DialogHelpButton
)
538 return theme_icon('list-add', fallback
='add.svg')
542 return theme_icon('list-remove', fallback
='remove.svg')
545 def open_file_icon():
546 return theme_icon('document-open', fallback
='open.svg')
550 """Return a standard open directory icon"""
551 return theme_icon('configure', fallback
='options.svg')
555 """Return a filter icon"""
556 return theme_icon('view-filter.png')
559 def dir_close_icon():
560 """Return a standard closed directory icon"""
561 return cached_icon(QtGui
.QStyle
.SP_DirClosedIcon
)
564 def titlebar_close_icon():
565 """Return a dock widget close icon"""
566 return cached_icon(QtGui
.QStyle
.SP_TitleBarCloseButton
)
569 def titlebar_normal_icon():
570 """Return a dock widget close icon"""
571 return cached_icon(QtGui
.QStyle
.SP_TitleBarNormalButton
)
576 Return git-cola icon from X11 theme if it exists.
577 Else fallback to default hardcoded icon.
579 return theme_icon('git-cola.svg')
583 """Returna standard Refresh icon"""
584 return cached_icon(QtGui
.QStyle
.SP_BrowserReload
)
588 """Return a standard Discard icon"""
589 return cached_icon(QtGui
.QStyle
.SP_DialogDiscardButton
)
593 """Return a standard Close icon"""
594 return cached_icon(QtGui
.QStyle
.SP_DialogCloseButton
)
597 def add_close_action(widget
):
598 """Adds close action and shortcuts to a widget."""
599 return add_action(widget
, N_('Close...'),
600 widget
.close
, QtGui
.QKeySequence
.Close
, 'Ctrl+Q')
603 def center_on_screen(widget
):
604 """Move widget to the center of the default screen"""
605 desktop
= QtGui
.QApplication
.instance().desktop()
606 rect
= desktop
.screenGeometry(QtGui
.QCursor().pos())
607 cy
= rect
.height()//2
609 widget
.move(cx
- widget
.width()//2, cy
- widget
.height()//2)
612 def default_size(parent
, width
, height
):
613 """Return the parent's size, or the provided defaults"""
614 if parent
is not None:
615 width
= parent
.width()
616 height
= parent
.height()
617 return (width
, height
)
620 def theme_icon(name
, fallback
=None):
621 """Grab an icon from the current theme with a fallback
623 Support older versions of Qt checking for fromTheme's availability.
626 if hasattr(QtGui
.QIcon
, 'fromTheme'):
627 base
, ext
= os
.path
.splitext(name
)
629 qicon
= QtGui
.QIcon
.fromTheme(base
, icon(fallback
))
631 qicon
= QtGui
.QIcon
.fromTheme(base
)
632 if not qicon
.isNull():
634 return icon(fallback
or name
)
637 def default_monospace_font():
640 if utils
.is_darwin():
642 font
.setFamily(family
)
647 font_str
= gitcfg
.current().get(FONTDIFF
)
649 font
= default_monospace_font()
650 font_str
= ustr(font
.toString())
655 font_str
= diff_font_str()
657 font
.fromString(font_str
)
661 def create_button(text
='', layout
=None, tooltip
=None, icon
=None):
662 """Create a button, set its title, and add it to the parent."""
663 button
= QtGui
.QPushButton()
664 button
.setCursor(Qt
.PointingHandCursor
)
669 if tooltip
is not None:
670 button
.setToolTip(tooltip
)
671 if layout
is not None:
672 layout
.addWidget(button
)
676 def create_action_button(tooltip
=None, icon
=None):
677 button
= QtGui
.QPushButton()
678 button
.setFixedSize(QtCore
.QSize(16, 16))
679 button
.setCursor(Qt
.PointingHandCursor
)
681 if tooltip
is not None:
682 button
.setToolTip(tooltip
)
684 pixmap
= icon
.pixmap(QtCore
.QSize(16, 16))
685 button
.setIcon(QtGui
.QIcon(pixmap
))
689 def hide_button_menu_indicator(button
):
693 %(name)s::menu-indicator {
697 if name
== 'QPushButton':
703 button
.setStyleSheet(stylesheet
% {'name': name
})
706 class DockTitleBarWidget(QtGui
.QWidget
):
708 def __init__(self
, parent
, title
, stretch
=True):
709 QtGui
.QWidget
.__init
__(self
, parent
)
710 self
.label
= label
= QtGui
.QLabel()
716 self
.setCursor(Qt
.OpenHandCursor
)
718 self
.close_button
= create_action_button(
719 tooltip
=N_('Close'), icon
=titlebar_close_icon())
721 self
.toggle_button
= create_action_button(
722 tooltip
=N_('Detach'), icon
=titlebar_normal_icon())
724 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
731 self
.main_layout
= hbox(defs
.small_margin
, defs
.spacing
,
732 label
, separator
, self
.corner_layout
,
733 self
.toggle_button
, self
.close_button
)
734 self
.setLayout(self
.main_layout
)
736 connect_button(self
.toggle_button
, self
.toggle_floating
)
737 connect_button(self
.close_button
, self
.toggle_visibility
)
739 def toggle_floating(self
):
740 self
.parent().setFloating(not self
.parent().isFloating())
741 self
.update_tooltips()
743 def toggle_visibility(self
):
744 self
.parent().toggleViewAction().trigger()
746 def set_title(self
, title
):
747 self
.label
.setText(title
)
749 def add_corner_widget(self
, widget
):
750 self
.corner_layout
.addWidget(widget
)
752 def update_tooltips(self
):
753 if self
.parent().isFloating():
754 tooltip
= N_('Attach')
756 tooltip
= N_('Detach')
757 self
.toggle_button
.setToolTip(tooltip
)
760 def create_dock(title
, parent
, stretch
=True):
761 """Create a dock widget and set it up accordingly."""
762 dock
= QtGui
.QDockWidget(parent
)
763 dock
.setWindowTitle(title
)
764 dock
.setObjectName(title
)
765 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
766 dock
.setTitleBarWidget(titlebar
)
767 if hasattr(parent
, 'dockwidgets'):
768 parent
.dockwidgets
.append(dock
)
772 def create_menu(title
, parent
):
773 """Create a menu and set its title."""
774 qmenu
= QtGui
.QMenu(parent
)
775 qmenu
.setTitle(title
)
779 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
780 button
= QtGui
.QToolButton()
781 button
.setAutoRaise(True)
782 button
.setAutoFillBackground(True)
783 button
.setCursor(Qt
.PointingHandCursor
)
788 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
789 if tooltip
is not None:
790 button
.setToolTip(tooltip
)
791 if layout
is not None:
792 layout
.addWidget(button
)
796 def mimedata_from_paths(paths
):
797 """Return mimedata with a list of absolute path URLs"""
799 abspaths
= [core
.abspath(path
) for path
in paths
]
800 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
802 mimedata
= QtCore
.QMimeData()
803 mimedata
.setUrls(urls
)
805 # The text/x-moz-list format is always included by Qt, and doing
806 # mimedata.removeFormat('text/x-moz-url') has no effect.
807 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
809 # gnome-terminal expects utf-16 encoded text, but other terminals,
810 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
811 # to override the default.
812 paths_text
= subprocess
.list2cmdline(abspaths
)
813 encoding
= gitcfg
.current().get('cola.dragencoding', 'utf-16')
814 moz_text
= core
.encode(paths_text
, encoding
=encoding
)
815 mimedata
.setData('text/x-moz-url', moz_text
)
820 def path_mimetypes():
821 return ['text/uri-list', 'text/x-moz-url']
823 # Syntax highlighting
825 def rgba(r
, g
, b
, a
=255):
836 def make_format(fg
=None, bg
=None, bold
=False):
837 fmt
= QtGui
.QTextCharFormat()
839 fmt
.setForeground(fg
)
841 fmt
.setBackground(bg
)
843 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
848 Interaction
.critical
= staticmethod(critical
)
849 Interaction
.confirm
= staticmethod(confirm
)
850 Interaction
.question
= staticmethod(question
)
851 Interaction
.information
= staticmethod(information
)