widgets: persist the clipboard when shutting down
[git-cola.git] / cola / qtutils.py
blob2e7b5b42f7ed8bfdcae048cde6f6d9038842e58d
1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
3 """
4 from __future__ import division, absolute_import, unicode_literals
6 import mimetypes
7 import os
8 import subprocess
10 from PyQt4 import QtGui
11 from PyQt4 import QtCore
12 from PyQt4.QtCore import Qt
13 from PyQt4.QtCore import SIGNAL
15 from cola import core
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',
40 '.cpp': 'script.png',
41 '.c': 'script.png',
42 '.h': 'script.png',
43 '.cxx': '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)
71 def active_window():
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)
83 STRETCH = object()
84 SKIPPED = object()
87 def box(cls, margin, spacing, *items):
88 stretch = STRETCH
89 skipped = SKIPPED
90 layout = cls()
91 layout.setMargin(margin)
92 layout.setSpacing(spacing)
94 for i in items:
95 if i is stretch:
96 layout.addStretch()
97 elif i is skipped:
98 continue
99 elif isinstance(i, QtGui.QWidget):
100 layout.addWidget(i)
101 elif isinstance(i, (QtGui.QHBoxLayout, QtGui.QVBoxLayout,
102 QtGui.QFormLayout, QtGui.QLayout)):
103 layout.addLayout(i)
104 elif isinstance(i, (int, long)):
105 layout.addSpacing(i)
107 return layout
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)
119 else:
120 layout.setWidget(idx, QtGui.QFormLayout.LabelRole, label)
121 layout.setWidget(idx, QtGui.QFormLayout.FieldRole, widget)
123 return layout
126 def grid(margin, spacing, *widgets):
127 layout = QtGui.QGridLayout()
128 layout.setMargin(defs.no_margin)
129 layout.setSpacing(defs.spacing)
131 for row in widgets:
132 item = row[0]
133 if isinstance(item, QtGui.QWidget):
134 layout.addWidget(*row)
135 elif isinstance(item, QtGui.QLayoutItem):
136 layout.addItem(*row)
138 return layout
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)
150 return layout
152 def prompt(msg, title=None, text=''):
153 """Presents the user with an input widget and returns the input."""
154 if title is None:
155 title = msg
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))
165 item.setText(text)
166 return item
169 class TreeWidgetItem(QtGui.QTreeWidgetItem):
171 TYPE = QtGui.QStandardItem.UserType + 101
173 def __init__(self, path, icon, deleted):
174 QtGui.QTreeWidgetItem.__init__(self)
175 self.path = path
176 self.deleted = deleted
177 self.setIcon(0, cached_icon_from_path(icon))
178 self.setText(0, path)
180 def type(self):
181 return self.TYPE
184 def paths_from_indexes(model, indexes,
185 item_type=TreeWidgetItem.TYPE,
186 item_filter=None):
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,
194 item_filter=None):
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)]
202 @memoize
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:
209 icon = default()
210 elif icon and isinstance(icon, (str, ustr)):
211 icon = QtGui.QIcon(icon)
212 return 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)
222 msgbox.setText(text)
223 msgbox.setInformativeText(informative_text)
225 icon = mkicon(icon, ok_icon)
226 ok = msgbox.addButton(ok_text, QtGui.QMessageBox.ActionRole)
227 ok.setIcon(icon)
229 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
230 cancel_icon = mkicon(cancel_icon, discard_icon)
231 cancel.setIcon(cancel_icon)
232 if cancel_text:
233 cancel.setText(cancel_text)
235 if default:
236 msgbox.setDefaultButton(ok)
237 else:
238 msgbox.setDefaultButton(cancel)
239 msgbox.exec_()
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)
258 if text is not None:
259 expand = QtGui.QSizePolicy.Expanding
260 text.setSizePolicy(QtGui.QSizePolicy(expand, expand))
261 text.setMaximumSize(maxi)
262 return res
265 def critical(title, message=None, details=None):
266 """Show a warning with the provided title and message."""
267 if message is None:
268 message = title
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)
276 if details:
277 mbox.setDetailedText(details)
278 mbox.exec_()
281 def information(title, message=None, details=None, informative_text=None):
282 """Show information with the provided title and message."""
283 if message is None:
284 message = title
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)
292 if informative_text:
293 mbox.setInformativeText(informative_text)
294 if details:
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)
299 mbox.exec_()
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
307 buttons = yes | no
308 if default:
309 default = yes
310 else:
311 default = 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"""
320 selected = []
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])
326 return selected
329 def tree_selection_items(tree_item):
330 """Returns selected widget items"""
331 selected = []
332 for idx in range(tree_item.childCount()):
333 child = tree_item.child(idx)
334 if child.isSelected():
335 selected.append(child)
337 return selected
340 def selected_item(list_widget, items):
341 """Returns the model item that corresponds to the selected QListWidget
342 row."""
343 widget_items = list_widget.selectedItems()
344 if not widget_items:
345 return None
346 widget_item = widget_items[0]
347 row = list_widget.row(widget_item)
348 if row < len(items):
349 return items[row]
350 else:
351 return None
354 def selected_items(list_widget, items):
355 """Returns an array of model items that correspond to the selected
356 QListWidget rows."""
357 item_count = len(items)
358 selected = []
359 for widget_item in list_widget.selectedItems():
360 row = list_widget.row(widget_item)
361 if row < item_count:
362 selected.append(items[row])
363 return selected
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(),
385 title, path, flags))
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))
394 def icon(basename):
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"""
401 if filename is None:
402 return
403 if absolute:
404 filename = core.abspath(filename)
405 set_clipboard(filename)
408 def set_clipboard(text):
409 """Sets the copy/paste buffer to text."""
410 if not text:
411 return
412 clipboard = QtGui.QApplication.clipboard()
413 clipboard.setText(text, QtGui.QClipboard.Clipboard)
414 clipboard.setText(text, QtGui.QClipboard.Selection)
415 persist_clipboard()
418 def persist_clipboard():
419 """Persist the clipboard
421 X11 stores only a reference to the clipboard data.
422 Send a clipboard event to force a copy of the clipboard to occur.
423 This ensures that the clipboard is present after git-cola exits.
424 Otherwise, the reference is destroyed on exit.
426 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
429 clipboard = QtGui.QApplication.clipboard()
430 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
431 QtGui.QApplication.sendEvent(clipboard, event)
434 def add_action_bool(widget, text, fn, checked, *shortcuts):
435 action = _add_action(widget, text, fn, connect_action_bool, *shortcuts)
436 action.setCheckable(True)
437 action.setChecked(checked)
438 return action
441 def add_action(widget, text, fn, *shortcuts):
442 return _add_action(widget, text, fn, connect_action, *shortcuts)
445 def _add_action(widget, text, fn, connect, *shortcuts):
446 action = QtGui.QAction(text, widget)
447 connect(action, fn)
448 if shortcuts:
449 action.setShortcuts(shortcuts)
450 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
451 widget.addAction(action)
452 return action
455 def set_selected_item(widget, idx):
456 """Sets a the currently selected item to the item at index idx."""
457 if type(widget) is QtGui.QTreeWidget:
458 item = widget.topLevelItem(idx)
459 if item:
460 widget.setItemSelected(item, True)
461 widget.setCurrentItem(item)
464 def add_items(widget, items):
465 """Adds items to a widget."""
466 for item in items:
467 widget.addItem(item)
470 def set_items(widget, items):
471 """Clear the existing widget contents and set the new items."""
472 widget.clear()
473 add_items(widget, items)
476 def icon_name_for_filename(filename):
477 """Returns an icon name based on the filename."""
478 mimetype = mimetypes.guess_type(filename)[0]
479 if mimetype is not None:
480 mimetype = mimetype.lower()
481 for filetype, icon_name in KNOWN_FILE_MIME_TYPES:
482 if filetype in mimetype:
483 return icon_name
484 extension = os.path.splitext(filename)[1]
485 return KNOWN_FILE_EXTENSIONS.get(extension.lower(), 'generic.png')
488 def icon_from_filename(filename):
489 icon_name = icon_name_for_filename(filename)
490 return cached_icon_from_path(resources.icon(icon_name))
493 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
494 """Given a filename, return a TreeListItem suitable for adding to a
495 QListWidget. "staged", "deleted, and "untracked" control whether to use
496 the appropriate icons."""
497 if deleted:
498 icon_name = 'removed.png'
499 elif staged:
500 icon_name = 'staged-item.png'
501 elif untracked:
502 icon_name = 'untracked.png'
503 else:
504 icon_name = icon_name_for_filename(filename)
505 return TreeWidgetItem(filename, resources.icon(icon_name), deleted=deleted)
508 @memoize
509 def cached_icon(key):
510 """Maintain a cache of standard icons and return cache entries."""
511 style = QtGui.QApplication.instance().style()
512 return style.standardIcon(key)
515 def dir_icon():
516 """Return a standard icon for a directory."""
517 return cached_icon(QtGui.QStyle.SP_DirIcon)
520 def file_icon():
521 """Return a standard icon for a file."""
522 return cached_icon(QtGui.QStyle.SP_FileIcon)
525 def apply_icon():
526 """Return a standard Apply icon"""
527 return cached_icon(QtGui.QStyle.SP_DialogApplyButton)
530 def new_icon():
531 return cached_icon(QtGui.QStyle.SP_FileDialogNewFolder)
534 def save_icon():
535 """Return a standard Save icon"""
536 return cached_icon(QtGui.QStyle.SP_DialogSaveButton)
539 def ok_icon():
540 """Return a standard Ok icon"""
541 return cached_icon(QtGui.QStyle.SP_DialogOkButton)
544 def open_icon():
545 """Return a standard open directory icon"""
546 return cached_icon(QtGui.QStyle.SP_DirOpenIcon)
549 def help_icon():
550 """Return a standard open directory icon"""
551 return cached_icon(QtGui.QStyle.SP_DialogHelpButton)
554 def add_icon():
555 return theme_icon('list-add', fallback='add.svg')
558 def remove_icon():
559 return theme_icon('list-remove', fallback='remove.svg')
562 def open_file_icon():
563 return theme_icon('document-open', fallback='open.svg')
566 def options_icon():
567 """Return a standard open directory icon"""
568 return theme_icon('configure', fallback='options.svg')
571 def filter_icon():
572 """Return a filter icon"""
573 return theme_icon('view-filter.png')
576 def dir_close_icon():
577 """Return a standard closed directory icon"""
578 return cached_icon(QtGui.QStyle.SP_DirClosedIcon)
581 def titlebar_close_icon():
582 """Return a dock widget close icon"""
583 return cached_icon(QtGui.QStyle.SP_TitleBarCloseButton)
586 def titlebar_normal_icon():
587 """Return a dock widget close icon"""
588 return cached_icon(QtGui.QStyle.SP_TitleBarNormalButton)
591 def git_icon():
593 Return git-cola icon from X11 theme if it exists.
594 Else fallback to default hardcoded icon.
596 return theme_icon('git-cola.svg')
599 def reload_icon():
600 """Returna standard Refresh icon"""
601 return cached_icon(QtGui.QStyle.SP_BrowserReload)
604 def discard_icon():
605 """Return a standard Discard icon"""
606 return cached_icon(QtGui.QStyle.SP_DialogDiscardButton)
609 def close_icon():
610 """Return a standard Close icon"""
611 return cached_icon(QtGui.QStyle.SP_DialogCloseButton)
614 def add_close_action(widget):
615 """Adds close action and shortcuts to a widget."""
616 return add_action(widget, N_('Close...'),
617 widget.close, QtGui.QKeySequence.Close, 'Ctrl+Q')
620 def center_on_screen(widget):
621 """Move widget to the center of the default screen"""
622 desktop = QtGui.QApplication.instance().desktop()
623 rect = desktop.screenGeometry(QtGui.QCursor().pos())
624 cy = rect.height()//2
625 cx = rect.width()//2
626 widget.move(cx - widget.width()//2, cy - widget.height()//2)
629 def default_size(parent, width, height):
630 """Return the parent's size, or the provided defaults"""
631 if parent is not None:
632 width = parent.width()
633 height = parent.height()
634 return (width, height)
636 @memoize
637 def theme_icon(name, fallback=None):
638 """Grab an icon from the current theme with a fallback
640 Support older versions of Qt checking for fromTheme's availability.
643 if hasattr(QtGui.QIcon, 'fromTheme'):
644 base, ext = os.path.splitext(name)
645 if fallback:
646 qicon = QtGui.QIcon.fromTheme(base, icon(fallback))
647 else:
648 qicon = QtGui.QIcon.fromTheme(base)
649 if not qicon.isNull():
650 return qicon
651 return icon(fallback or name)
654 def default_monospace_font():
655 font = QtGui.QFont()
656 family = 'Monospace'
657 if utils.is_darwin():
658 family = 'Monaco'
659 font.setFamily(family)
660 return font
663 def diff_font_str():
664 font_str = gitcfg.current().get(FONTDIFF)
665 if font_str is None:
666 font = default_monospace_font()
667 font_str = ustr(font.toString())
668 return font_str
671 def diff_font():
672 font_str = diff_font_str()
673 font = QtGui.QFont()
674 font.fromString(font_str)
675 return font
678 def create_button(text='', layout=None, tooltip=None, icon=None):
679 """Create a button, set its title, and add it to the parent."""
680 button = QtGui.QPushButton()
681 button.setCursor(Qt.PointingHandCursor)
682 if text:
683 button.setText(text)
684 if icon is not None:
685 button.setIcon(icon)
686 if tooltip is not None:
687 button.setToolTip(tooltip)
688 if layout is not None:
689 layout.addWidget(button)
690 return button
693 def create_action_button(tooltip=None, icon=None):
694 button = QtGui.QPushButton()
695 button.setFixedSize(QtCore.QSize(16, 16))
696 button.setCursor(Qt.PointingHandCursor)
697 button.setFlat(True)
698 if tooltip is not None:
699 button.setToolTip(tooltip)
700 if icon is not None:
701 pixmap = icon.pixmap(QtCore.QSize(16, 16))
702 button.setIcon(QtGui.QIcon(pixmap))
703 return button
706 def hide_button_menu_indicator(button):
707 cls = type(button)
708 name = cls.__name__
709 stylesheet = """
710 %(name)s::menu-indicator {
711 image: none;
714 if name == 'QPushButton':
715 stylesheet += """
716 %(name)s {
717 border-style: none;
720 button.setStyleSheet(stylesheet % {'name': name})
723 class DockTitleBarWidget(QtGui.QWidget):
725 def __init__(self, parent, title, stretch=True):
726 QtGui.QWidget.__init__(self, parent)
727 self.label = label = QtGui.QLabel()
728 font = label.font()
729 font.setBold(True)
730 label.setFont(font)
731 label.setText(title)
733 self.setCursor(Qt.OpenHandCursor)
735 self.close_button = create_action_button(
736 tooltip=N_('Close'), icon=titlebar_close_icon())
738 self.toggle_button = create_action_button(
739 tooltip=N_('Detach'), icon=titlebar_normal_icon())
741 self.corner_layout = hbox(defs.no_margin, defs.spacing)
743 if stretch:
744 separator = STRETCH
745 else:
746 separator = SKIPPED
748 self.main_layout = hbox(defs.small_margin, defs.spacing,
749 label, separator, self.corner_layout,
750 self.toggle_button, self.close_button)
751 self.setLayout(self.main_layout)
753 connect_button(self.toggle_button, self.toggle_floating)
754 connect_button(self.close_button, self.toggle_visibility)
756 def toggle_floating(self):
757 self.parent().setFloating(not self.parent().isFloating())
758 self.update_tooltips()
760 def toggle_visibility(self):
761 self.parent().toggleViewAction().trigger()
763 def set_title(self, title):
764 self.label.setText(title)
766 def add_corner_widget(self, widget):
767 self.corner_layout.addWidget(widget)
769 def update_tooltips(self):
770 if self.parent().isFloating():
771 tooltip = N_('Attach')
772 else:
773 tooltip = N_('Detach')
774 self.toggle_button.setToolTip(tooltip)
777 def create_dock(title, parent, stretch=True):
778 """Create a dock widget and set it up accordingly."""
779 dock = QtGui.QDockWidget(parent)
780 dock.setWindowTitle(title)
781 dock.setObjectName(title)
782 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
783 dock.setTitleBarWidget(titlebar)
784 if hasattr(parent, 'dockwidgets'):
785 parent.dockwidgets.append(dock)
786 return dock
789 def create_menu(title, parent):
790 """Create a menu and set its title."""
791 qmenu = QtGui.QMenu(parent)
792 qmenu.setTitle(title)
793 return qmenu
796 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
797 button = QtGui.QToolButton()
798 button.setAutoRaise(True)
799 button.setAutoFillBackground(True)
800 button.setCursor(Qt.PointingHandCursor)
801 if icon is not None:
802 button.setIcon(icon)
803 if text is not None:
804 button.setText(text)
805 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
806 if tooltip is not None:
807 button.setToolTip(tooltip)
808 if layout is not None:
809 layout.addWidget(button)
810 return button
813 def mimedata_from_paths(paths):
814 """Return mimedata with a list of absolute path URLs"""
816 abspaths = [core.abspath(path) for path in paths]
817 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
819 mimedata = QtCore.QMimeData()
820 mimedata.setUrls(urls)
822 # The text/x-moz-list format is always included by Qt, and doing
823 # mimedata.removeFormat('text/x-moz-url') has no effect.
824 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
826 # gnome-terminal expects utf-16 encoded text, but other terminals,
827 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
828 # to override the default.
829 paths_text = subprocess.list2cmdline(abspaths)
830 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
831 moz_text = core.encode(paths_text, encoding=encoding)
832 mimedata.setData('text/x-moz-url', moz_text)
834 return mimedata
837 def path_mimetypes():
838 return ['text/uri-list', 'text/x-moz-url']
841 class BlockSignals(object):
842 """Context manager for blocking a signals on a widget"""
844 def __init__(self, *widgets):
845 self.widgets = widgets
846 self.values = {}
848 def __enter__(self):
849 for w in self.widgets:
850 self.values[w] = w.blockSignals(True)
851 return self
853 def __exit__(self, exc_type, exc_val, exc_tb):
854 for w in self.widgets:
855 w.blockSignals(self.values[w])
858 # Syntax highlighting
860 def rgba(r, g, b, a=255):
861 c = QtGui.QColor()
862 c.setRgb(r, g, b)
863 c.setAlpha(a)
864 return c
867 def RGB(args):
868 return rgba(*args)
871 def make_format(fg=None, bg=None, bold=False):
872 fmt = QtGui.QTextCharFormat()
873 if fg:
874 fmt.setForeground(fg)
875 if bg:
876 fmt.setBackground(bg)
877 if bold:
878 fmt.setFontWeight(QtGui.QFont.Bold)
879 return fmt
882 def install():
883 Interaction.critical = staticmethod(critical)
884 Interaction.confirm = staticmethod(confirm)
885 Interaction.question = staticmethod(question)
886 Interaction.information = staticmethod(information)