dag: add commits_were_invalidated method to Edge
[git-cola.git] / cola / qtutils.py
blob0b4fa6c16520e8aa59a66a87f954eef76571825d
1 # Copyright (c) 2008-2016 David Aguilar
2 """Miscellaneous Qt utility functions."""
3 from __future__ import division, absolute_import, unicode_literals
5 from qtpy import compat
6 from qtpy import QtGui
7 from qtpy import QtCore
8 from qtpy import QtWidgets
9 from qtpy.QtCore import Qt
10 from qtpy.QtCore import Signal
12 from . import core
13 from . import gitcfg
14 from . import hotkeys
15 from . import icons
16 from . import utils
17 from .i18n import N_
18 from .interaction import Interaction
19 from .compat import int_types
20 from .compat import ustr
21 from .models import prefs
22 from .widgets import defs
25 STRETCH = object()
26 SKIPPED = object()
29 def disconnect(signal):
30 """Disconnect signal from all slots"""
31 try:
32 signal.disconnect()
33 except TypeError: # allow unconnected slots
34 pass
37 def connect_action(action, fn):
38 """Connect an action to a function"""
39 action.triggered[bool].connect(lambda x: fn())
42 def connect_action_bool(action, fn):
43 """Connect a triggered(bool) action to a function"""
44 action.triggered[bool].connect(fn)
47 def connect_button(button, fn):
48 """Connect a button to a function"""
49 button.pressed.connect(fn)
52 def button_action(button, action):
53 """Make a button trigger an action"""
54 connect_button(button, action.trigger)
57 def connect_toggle(toggle, fn):
58 """Connect a toggle button to a function"""
59 toggle.toggled.connect(fn)
62 def active_window():
63 """Return the active window for the current application"""
64 return QtWidgets.QApplication.activeWindow()
67 def hbox(margin, spacing, *items):
68 """Create an HBoxLayout with the specified sizes and items"""
69 return box(QtWidgets.QHBoxLayout, margin, spacing, *items)
72 def vbox(margin, spacing, *items):
73 """Create a VBoxLayout with the specified sizes and items"""
74 return box(QtWidgets.QVBoxLayout, margin, spacing, *items)
77 def buttongroup(*items):
78 """Create a QButtonGroup for the specified items"""
79 group = QtWidgets.QButtonGroup()
80 for i in items:
81 group.addButton(i)
82 return group
85 def set_margin(layout, margin):
86 """Set the content margins for a layout"""
87 layout.setContentsMargins(margin, margin, margin, margin)
90 def box(cls, margin, spacing, *items):
91 """Create a QBoxLayout with the specified sizes and items"""
92 stretch = STRETCH
93 skipped = SKIPPED
94 layout = cls()
95 layout.setSpacing(spacing)
96 set_margin(layout, margin)
98 for i in items:
99 if isinstance(i, QtWidgets.QWidget):
100 layout.addWidget(i)
101 elif isinstance(i, (QtWidgets.QHBoxLayout, QtWidgets.QVBoxLayout,
102 QtWidgets.QFormLayout, QtWidgets.QLayout)):
103 layout.addLayout(i)
104 elif i is stretch:
105 layout.addStretch()
106 elif i is skipped:
107 continue
108 elif isinstance(i, int_types):
109 layout.addSpacing(i)
111 return layout
114 def form(margin, spacing, *widgets):
115 """Create a QFormLayout with the specified sizes and items"""
116 layout = QtWidgets.QFormLayout()
117 layout.setSpacing(spacing)
118 layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
119 set_margin(layout, margin)
121 for idx, (name, widget) in enumerate(widgets):
122 if isinstance(name, (str, ustr)):
123 layout.addRow(name, widget)
124 else:
125 layout.setWidget(idx, QtWidgets.QFormLayout.LabelRole, name)
126 layout.setWidget(idx, QtWidgets.QFormLayout.FieldRole, widget)
128 return layout
131 def grid(margin, spacing, *widgets):
132 """Create a QGridLayout with the specified sizes and items"""
133 layout = QtWidgets.QGridLayout()
134 layout.setSpacing(spacing)
135 set_margin(layout, margin)
137 for row in widgets:
138 item = row[0]
139 if isinstance(item, QtWidgets.QWidget):
140 layout.addWidget(*row)
141 elif isinstance(item, QtWidgets.QLayoutItem):
142 layout.addItem(*row)
144 return layout
147 def splitter(orientation, *widgets):
148 """Create a spliter over the specified widgets
150 :param orientation: Qt.Horizontal or Qt.Vertical
153 layout = QtWidgets.QSplitter()
154 layout.setOrientation(orientation)
155 layout.setHandleWidth(defs.handle_width)
156 layout.setChildrenCollapsible(True)
158 for idx, widget in enumerate(widgets):
159 layout.addWidget(widget)
160 layout.setStretchFactor(idx, 1)
162 # Workaround for Qt not setting the WA_Hover property for QSplitter
163 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
164 layout.handle(1).setAttribute(Qt.WA_Hover)
166 return layout
169 def label(text=None, align=None, fmt=None, selectable=True):
170 """Create a QLabel with the specified properties"""
171 widget = QtWidgets.QLabel()
172 if align is not None:
173 widget.setAlignment(align)
174 if fmt is not None:
175 widget.setTextFormat(fmt)
176 if selectable:
177 widget.setTextInteractionFlags(Qt.TextBrowserInteraction)
178 widget.setOpenExternalLinks(True)
179 if text:
180 widget.setText(text)
181 return widget
184 def textbrowser(text=None):
185 """Create a QTextBrowser for the specified text"""
186 widget = QtWidgets.QTextBrowser()
187 widget.setOpenExternalLinks(True)
188 if text:
189 widget.setText(text)
190 return widget
193 def add_completer(widget, items):
194 """Add simple completion to a widget"""
195 completer = QtWidgets.QCompleter(items, widget)
196 completer.setCaseSensitivity(Qt.CaseInsensitive)
197 completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion)
198 widget.setCompleter(completer)
201 def prompt(msg, title=None, text=''):
202 """Presents the user with an input widget and returns the input."""
203 if title is None:
204 title = msg
205 result = QtWidgets.QInputDialog.getText(
206 active_window(), msg, title,
207 QtWidgets.QLineEdit.Normal, text)
208 return (result[0], result[1])
211 def prompt_n(msg, inputs):
212 """Presents the user with N input widgets and returns the results"""
213 dialog = QtWidgets.QDialog(active_window())
214 dialog.setWindowModality(Qt.WindowModal)
215 dialog.setWindowTitle(msg)
217 long_value = msg
218 for k, v in inputs:
219 if len(k + v) > len(long_value):
220 long_value = k + v
222 metrics = QtGui.QFontMetrics(dialog.font())
223 min_width = metrics.width(long_value) + 100
224 if min_width > 720:
225 min_width = 720
226 dialog.setMinimumWidth(min_width)
228 ok_b = ok_button(msg, enabled=False)
229 close_b = close_button()
231 form_widgets = []
233 def get_values():
234 return [pair[1].text().strip() for pair in form_widgets]
236 for name, value in inputs:
237 lineedit = QtWidgets.QLineEdit()
238 # Enable the OK button only when all fields have been populated
239 lineedit.textChanged.connect(
240 lambda x: ok_b.setEnabled(all(get_values())))
241 if value:
242 lineedit.setText(value)
243 form_widgets.append((name, lineedit))
245 # layouts
246 form_layout = form(defs.no_margin, defs.button_spacing, *form_widgets)
247 button_layout = hbox(defs.no_margin, defs.button_spacing,
248 STRETCH, close_b, ok_b)
249 main_layout = vbox(defs.margin, defs.button_spacing,
250 form_layout, button_layout)
251 dialog.setLayout(main_layout)
253 # connections
254 connect_button(ok_b, dialog.accept)
255 connect_button(close_b, dialog.reject)
257 accepted = dialog.exec_() == QtWidgets.QDialog.Accepted
258 text = get_values()
259 ok = accepted and all(text)
260 return (ok, text)
263 class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
265 TYPE = QtGui.QStandardItem.UserType + 101
267 def __init__(self, path, icon, deleted):
268 QtWidgets.QTreeWidgetItem.__init__(self)
269 self.path = path
270 self.deleted = deleted
271 self.setIcon(0, icons.from_name(icon))
272 self.setText(0, path)
274 def type(self):
275 return self.TYPE
278 def paths_from_indexes(model, indexes,
279 item_type=TreeWidgetItem.TYPE,
280 item_filter=None):
281 """Return paths from a list of QStandardItemModel indexes"""
282 items = [model.itemFromIndex(i) for i in indexes]
283 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
286 def _true_filter(x):
287 return True
290 def paths_from_items(items,
291 item_type=TreeWidgetItem.TYPE,
292 item_filter=None):
293 """Return a list of paths from a list of items"""
294 if item_filter is None:
295 item_filter = _true_filter
296 return [i.path for i in items
297 if i.type() == item_type and item_filter(i)]
300 def confirm(title, text, informative_text, ok_text,
301 icon=None, default=True,
302 cancel_text=None, cancel_icon=None):
303 """Confirm that an action should take place"""
304 msgbox = QtWidgets.QMessageBox(active_window())
305 msgbox.setWindowModality(Qt.WindowModal)
306 msgbox.setWindowTitle(title)
307 msgbox.setText(text)
308 msgbox.setInformativeText(informative_text)
310 icon = icons.mkicon(icon, icons.ok)
311 ok = msgbox.addButton(ok_text, QtWidgets.QMessageBox.ActionRole)
312 ok.setIcon(icon)
314 cancel = msgbox.addButton(QtWidgets.QMessageBox.Cancel)
315 cancel_icon = icons.mkicon(cancel_icon, icons.close)
316 cancel.setIcon(cancel_icon)
317 if cancel_text:
318 cancel.setText(cancel_text)
320 if default:
321 msgbox.setDefaultButton(ok)
322 else:
323 msgbox.setDefaultButton(cancel)
324 msgbox.exec_()
325 return msgbox.clickedButton() == ok
328 class ResizeableMessageBox(QtWidgets.QMessageBox):
330 def __init__(self, parent):
331 QtWidgets.QMessageBox.__init__(self, parent)
332 self.setMouseTracking(True)
333 self.setSizeGripEnabled(True)
335 def event(self, event):
336 res = QtWidgets.QMessageBox.event(self, event)
337 event_type = event.type()
338 if (event_type == QtCore.QEvent.MouseMove or
339 event_type == QtCore.QEvent.MouseButtonPress):
340 maxi = QtCore.QSize(defs.max_size, defs.max_size)
341 self.setMaximumSize(maxi)
342 text = self.findChild(QtWidgets.QTextEdit)
343 if text is not None:
344 expand = QtWidgets.QSizePolicy.Expanding
345 text.setSizePolicy(QtWidgets.QSizePolicy(expand, expand))
346 text.setMaximumSize(maxi)
347 return res
350 def critical(title, message=None, details=None):
351 """Show a warning with the provided title and message."""
352 if message is None:
353 message = title
354 mbox = ResizeableMessageBox(active_window())
355 mbox.setWindowTitle(title)
356 mbox.setTextFormat(Qt.PlainText)
357 mbox.setText(message)
358 mbox.setIcon(QtWidgets.QMessageBox.Critical)
359 mbox.setStandardButtons(QtWidgets.QMessageBox.Close)
360 mbox.setDefaultButton(QtWidgets.QMessageBox.Close)
361 if details:
362 mbox.setDetailedText(details)
363 mbox.exec_()
366 def information(title, message=None, details=None, informative_text=None):
367 """Show information with the provided title and message."""
368 if message is None:
369 message = title
370 mbox = QtWidgets.QMessageBox(active_window())
371 mbox.setStandardButtons(QtWidgets.QMessageBox.Close)
372 mbox.setDefaultButton(QtWidgets.QMessageBox.Close)
373 mbox.setWindowTitle(title)
374 mbox.setWindowModality(Qt.WindowModal)
375 mbox.setTextFormat(Qt.PlainText)
376 mbox.setText(message)
377 if informative_text:
378 mbox.setInformativeText(informative_text)
379 if details:
380 mbox.setDetailedText(details)
381 # Render into a 1-inch wide pixmap
382 pixmap = icons.cola().pixmap(defs.large_icon)
383 mbox.setIconPixmap(pixmap)
384 mbox.exec_()
387 def question(title, msg, default=True):
388 """Launches a QMessageBox question with the provided title and message.
389 Passing "default=False" will make "No" the default choice."""
390 yes = QtWidgets.QMessageBox.Yes
391 no = QtWidgets.QMessageBox.No
392 buttons = yes | no
393 if default:
394 default = yes
395 else:
396 default = no
398 parent = active_window()
399 MessageBox = QtWidgets.QMessageBox
400 result = MessageBox.question(parent, title, msg, buttons, default)
401 return result == QtWidgets.QMessageBox.Yes
404 def tree_selection(tree_item, items):
405 """Returns an array of model items that correspond to the selected
406 QTreeWidgetItem children"""
407 selected = []
408 count = min(tree_item.childCount(), len(items))
409 for idx in range(count):
410 if tree_item.child(idx).isSelected():
411 selected.append(items[idx])
413 return selected
416 def tree_selection_items(tree_item):
417 """Returns selected widget items"""
418 selected = []
419 for idx in range(tree_item.childCount()):
420 child = tree_item.child(idx)
421 if child.isSelected():
422 selected.append(child)
424 return selected
427 def selected_item(list_widget, items):
428 """Returns the model item that corresponds to the selected QListWidget
429 row."""
430 widget_items = list_widget.selectedItems()
431 if not widget_items:
432 return None
433 widget_item = widget_items[0]
434 row = list_widget.row(widget_item)
435 if row < len(items):
436 return items[row]
437 else:
438 return None
441 def selected_items(list_widget, items):
442 """Returns an array of model items that correspond to the selected
443 QListWidget rows."""
444 item_count = len(items)
445 selected = []
446 for widget_item in list_widget.selectedItems():
447 row = list_widget.row(widget_item)
448 if row < item_count:
449 selected.append(items[row])
450 return selected
453 def open_file(title, directory=None):
454 """Creates an Open File dialog and returns a filename."""
455 result = compat.getopenfilename(parent=active_window(),
456 caption=title,
457 basedir=directory)
458 return result[0]
461 def open_files(title, directory=None, filters=''):
462 """Creates an Open File dialog and returns a list of filenames."""
463 result = compat.getopenfilenames(parent=active_window(),
464 caption=title,
465 basedir=directory,
466 filters=filters)
467 return result[0]
470 def opendir_dialog(caption, path):
471 """Prompts for a directory path"""
473 options = (QtWidgets.QFileDialog.ShowDirsOnly |
474 QtWidgets.QFileDialog.DontResolveSymlinks)
475 return compat.getexistingdirectory(parent=active_window(),
476 caption=caption,
477 basedir=path,
478 options=options)
481 def save_as(filename, title='Save As...'):
482 """Creates a Save File dialog and returns a filename."""
483 result = compat.getsavefilename(parent=active_window(),
484 caption=title,
485 basedir=filename)
486 return result[0]
489 def copy_path(filename, absolute=True):
490 """Copy a filename to the clipboard"""
491 if filename is None:
492 return
493 if absolute:
494 filename = core.abspath(filename)
495 set_clipboard(filename)
498 def set_clipboard(text):
499 """Sets the copy/paste buffer to text."""
500 if not text:
501 return
502 clipboard = QtWidgets.QApplication.clipboard()
503 clipboard.setText(text, QtGui.QClipboard.Clipboard)
504 clipboard.setText(text, QtGui.QClipboard.Selection)
505 persist_clipboard()
508 def persist_clipboard():
509 """Persist the clipboard
511 X11 stores only a reference to the clipboard data.
512 Send a clipboard event to force a copy of the clipboard to occur.
513 This ensures that the clipboard is present after git-cola exits.
514 Otherwise, the reference is destroyed on exit.
516 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
519 clipboard = QtWidgets.QApplication.clipboard()
520 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
521 QtWidgets.QApplication.sendEvent(clipboard, event)
524 def add_action_bool(widget, text, fn, checked, *shortcuts):
525 tip = text
526 action = _add_action(widget, text, tip, fn, connect_action_bool, *shortcuts)
527 action.setCheckable(True)
528 action.setChecked(checked)
529 return action
532 def add_action(widget, text, fn, *shortcuts):
533 tip = text
534 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
537 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
538 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
541 def _add_action(widget, text, tip, fn, connect, *shortcuts):
542 action = QtWidgets.QAction(text, widget)
543 if hasattr(action, 'setIconVisibleInMenu'):
544 action.setIconVisibleInMenu(True)
545 if tip:
546 action.setStatusTip(tip)
547 connect(action, fn)
548 if shortcuts:
549 action.setShortcuts(shortcuts)
550 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
551 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
552 widget.addAction(action)
553 return action
556 def set_selected_item(widget, idx):
557 """Sets a the currently selected item to the item at index idx."""
558 if type(widget) is QtWidgets.QTreeWidget:
559 item = widget.topLevelItem(idx)
560 if item:
561 item.setSelected(True)
562 widget.setCurrentItem(item)
565 def add_items(widget, items):
566 """Adds items to a widget."""
567 for item in items:
568 if item is None:
569 continue
570 widget.addItem(item)
573 def set_items(widget, items):
574 """Clear the existing widget contents and set the new items."""
575 widget.clear()
576 add_items(widget, items)
579 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
580 """Given a filename, return a TreeWidgetItem for a status widget
582 "staged", "deleted, and "untracked" control which icon is used.
585 icon_name = icons.status(filename, deleted, staged, untracked)
586 return TreeWidgetItem(filename, icons.name_from_basename(icon_name),
587 deleted=deleted)
590 def add_close_action(widget):
591 """Adds close action and shortcuts to a widget."""
592 return add_action(widget, N_('Close...'),
593 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
596 def app():
597 """Return the current application"""
598 return QtWidgets.QApplication.instance()
601 def desktop():
602 """Return the desktop"""
603 return app().desktop()
606 def center_on_screen(widget):
607 """Move widget to the center of the default screen"""
608 desk = desktop()
609 rect = desk.screenGeometry(QtGui.QCursor().pos())
610 cy = rect.height()//2
611 cx = rect.width()//2
612 widget.move(cx - widget.width()//2, cy - widget.height()//2)
615 def default_size(parent, width, height):
616 """Return the parent's size, or the provided defaults"""
617 if parent is not None:
618 width = parent.width()
619 height = parent.height()
620 return (width, height)
623 def default_monospace_font():
624 font = QtGui.QFont()
625 family = 'Monospace'
626 if utils.is_darwin():
627 family = 'Monaco'
628 font.setFamily(family)
629 return font
632 def diff_font_str():
633 font_str = gitcfg.current().get(prefs.FONTDIFF)
634 if font_str is None:
635 font_str = default_monospace_font().toString()
636 return font_str
639 def diff_font():
640 return font(diff_font_str())
643 def font(string):
644 font = QtGui.QFont()
645 font.fromString(string)
646 return font
649 def create_button(text='', layout=None, tooltip=None, icon=None,
650 enabled=True, default=False):
651 """Create a button, set its title, and add it to the parent."""
652 button = QtWidgets.QPushButton()
653 button.setCursor(Qt.PointingHandCursor)
654 if text:
655 button.setText(text)
656 if icon is not None:
657 button.setIcon(icon)
658 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
659 if tooltip is not None:
660 button.setToolTip(tooltip)
661 if layout is not None:
662 layout.addWidget(button)
663 if not enabled:
664 button.setEnabled(False)
665 if default:
666 button.setDefault(True)
667 return button
670 def create_action_button(tooltip=None, icon=None):
671 button = QtWidgets.QPushButton()
672 button.setCursor(Qt.PointingHandCursor)
673 button.setFlat(True)
674 if tooltip is not None:
675 button.setToolTip(tooltip)
676 if icon is not None:
677 button.setIcon(icon)
678 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
679 return button
682 def ok_button(text, default=True, enabled=True, icon=None):
683 if icon is None:
684 icon = icons.ok()
685 return create_button(text=text, icon=icon, default=default, enabled=enabled)
688 def close_button():
689 return create_button(text=N_('Close'), icon=icons.close())
692 def edit_button(enabled=True, default=False):
693 return create_button(text=N_('Edit'), icon=icons.edit(),
694 enabled=enabled, default=default)
697 def refresh_button(enabled=True, default=False):
698 return create_button(text=N_('Refresh'), icon=icons.sync(),
699 enabled=enabled, default=default)
702 def hide_button_menu_indicator(button):
703 """Hide the menu indicator icon on buttons"""
705 name = button.__class__.__name__
706 stylesheet = """
707 %(name)s::menu-indicator {
708 image: none;
711 if name == 'QPushButton':
712 stylesheet += """
713 %(name)s {
714 border-style: none;
717 button.setStyleSheet(stylesheet % dict(name=name))
720 def checkbox(text='', tooltip='', checked=None):
721 """Create a checkbox"""
722 return _checkbox(QtWidgets.QCheckBox, text, tooltip, checked)
725 def radio(text='', tooltip='', checked=None):
726 """Create a radio button"""
727 return _checkbox(QtWidgets.QRadioButton, text, tooltip, checked)
730 def _checkbox(cls, text, tooltip, checked):
731 """Create a widget and apply properties"""
732 widget = cls()
733 if text:
734 widget.setText(text)
735 if tooltip:
736 widget.setToolTip(tooltip)
737 if checked is not None:
738 widget.setChecked(checked)
739 return widget
742 class DockTitleBarWidget(QtWidgets.QWidget):
744 def __init__(self, parent, title, stretch=True):
745 QtWidgets.QWidget.__init__(self, parent)
746 self.setAutoFillBackground(True)
747 self.label = qlabel = QtWidgets.QLabel(title, self)
748 font = qlabel.font()
749 font.setBold(True)
750 qlabel.setFont(font)
751 qlabel.setCursor(Qt.OpenHandCursor)
753 self.close_button = create_action_button(
754 tooltip=N_('Close'), icon=icons.close())
756 self.toggle_button = create_action_button(
757 tooltip=N_('Detach'), icon=icons.external())
759 self.corner_layout = hbox(defs.no_margin, defs.spacing)
761 if stretch:
762 separator = STRETCH
763 else:
764 separator = SKIPPED
766 self.main_layout = hbox(defs.small_margin, defs.spacing,
767 qlabel, separator, self.corner_layout,
768 self.toggle_button, self.close_button)
769 self.setLayout(self.main_layout)
771 connect_button(self.toggle_button, self.toggle_floating)
772 connect_button(self.close_button, self.toggle_visibility)
774 def toggle_floating(self):
775 self.parent().setFloating(not self.parent().isFloating())
776 self.update_tooltips()
778 def toggle_visibility(self):
779 self.parent().toggleViewAction().trigger()
781 def set_title(self, title):
782 self.label.setText(title)
784 def add_corner_widget(self, widget):
785 self.corner_layout.addWidget(widget)
787 def update_tooltips(self):
788 if self.parent().isFloating():
789 tooltip = N_('Attach')
790 else:
791 tooltip = N_('Detach')
792 self.toggle_button.setToolTip(tooltip)
795 def create_dock(title, parent, stretch=True):
796 """Create a dock widget and set it up accordingly."""
797 dock = QtWidgets.QDockWidget(parent)
798 dock.setWindowTitle(title)
799 dock.setObjectName(title)
800 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
801 dock.setTitleBarWidget(titlebar)
802 dock.setAutoFillBackground(True)
803 if hasattr(parent, 'dockwidgets'):
804 parent.dockwidgets.append(dock)
805 return dock
808 def create_menu(title, parent):
809 """Create a menu and set its title."""
810 qmenu = QtWidgets.QMenu(title, parent)
811 return qmenu
814 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
815 button = QtWidgets.QToolButton()
816 button.setAutoRaise(True)
817 button.setAutoFillBackground(True)
818 button.setCursor(Qt.PointingHandCursor)
819 if icon is not None:
820 button.setIcon(icon)
821 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
822 if text is not None:
823 button.setText(text)
824 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
825 if tooltip is not None:
826 button.setToolTip(tooltip)
827 if layout is not None:
828 layout.addWidget(button)
829 return button
832 def mimedata_from_paths(paths):
833 """Return mimedata with a list of absolute path URLs"""
835 abspaths = [core.abspath(path) for path in paths]
836 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
838 mimedata = QtCore.QMimeData()
839 mimedata.setUrls(urls)
841 # The text/x-moz-list format is always included by Qt, and doing
842 # mimedata.removeFormat('text/x-moz-url') has no effect.
843 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
845 # gnome-terminal expects utf-16 encoded text, but other terminals,
846 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
847 # to override the default.
848 paths_text = core.list2cmdline(abspaths)
849 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
850 moz_text = core.encode(paths_text, encoding=encoding)
851 mimedata.setData('text/x-moz-url', moz_text)
853 return mimedata
856 def path_mimetypes():
857 return ['text/uri-list', 'text/x-moz-url']
860 class BlockSignals(object):
861 """Context manager for blocking a signals on a widget"""
863 def __init__(self, *widgets):
864 self.widgets = widgets
865 self.values = {}
867 def __enter__(self):
868 for w in self.widgets:
869 self.values[w] = w.blockSignals(True)
870 return self
872 def __exit__(self, exc_type, exc_val, exc_tb):
873 for w in self.widgets:
874 w.blockSignals(self.values[w])
877 class Channel(QtCore.QObject):
878 finished = Signal(object)
879 result = Signal(object)
882 class Task(QtCore.QRunnable):
883 """Disable auto-deletion to avoid gc issues
885 Python's garbage collector will try to double-free the task
886 once it's finished, so disable Qt's auto-deletion as a workaround.
890 def __init__(self, parent):
891 QtCore.QRunnable.__init__(self)
893 self.channel = Channel()
894 self.result = None
895 self.setAutoDelete(False)
897 def run(self):
898 self.result = self.task()
899 self.channel.result.emit(self.result)
900 self.done()
902 def task(self):
903 return None
905 def done(self):
906 self.channel.finished.emit(self)
908 def connect(self, handler):
909 self.channel.result.connect(handler, type=Qt.QueuedConnection)
912 class SimpleTask(Task):
913 """Run a simple callable as a task"""
915 def __init__(self, parent, fn, *args, **kwargs):
916 Task.__init__(self, parent)
918 self.fn = fn
919 self.args = args
920 self.kwargs = kwargs
922 def task(self):
923 return self.fn(*self.args, **self.kwargs)
926 class RunTask(QtCore.QObject):
927 """Runs QRunnable instances and transfers control when they finish"""
929 def __init__(self, parent=None):
930 QtCore.QObject.__init__(self, parent)
931 self.tasks = []
932 self.task_details = {}
933 self.threadpool = QtCore.QThreadPool.globalInstance()
935 def start(self, task, progress=None, finish=None):
936 """Start the task and register a callback"""
937 if progress is not None:
938 progress.show()
939 # prevents garbage collection bugs in certain PyQt4 versions
940 self.tasks.append(task)
941 task_id = id(task)
942 self.task_details[task_id] = (progress, finish)
944 task.channel.finished.connect(self.finish, type=Qt.QueuedConnection)
945 self.threadpool.start(task)
947 def finish(self, task):
948 task_id = id(task)
949 try:
950 self.tasks.remove(task)
951 except:
952 pass
953 try:
954 progress, finish = self.task_details[task_id]
955 del self.task_details[task_id]
956 except KeyError:
957 finish = progress = None
959 if progress is not None:
960 progress.hide()
962 if finish is not None:
963 finish(task)
966 # Syntax highlighting
968 def rgba(r, g, b, a=255):
969 c = QtGui.QColor()
970 c.setRgb(r, g, b)
971 c.setAlpha(a)
972 return c
975 def RGB(args):
976 return rgba(*args)
979 def rgb_css(color):
980 """Convert a QColor into an rgb(int, int, int) CSS string"""
981 return 'rgb(%d, %d, %d)' % (color.red(), color.green(), color.blue())
984 def make_format(fg=None, bg=None, bold=False):
985 fmt = QtGui.QTextCharFormat()
986 if fg:
987 fmt.setForeground(fg)
988 if bg:
989 fmt.setBackground(bg)
990 if bold:
991 fmt.setFontWeight(QtGui.QFont.Bold)
992 return fmt
995 def install():
996 Interaction.critical = staticmethod(critical)
997 Interaction.confirm = staticmethod(confirm)
998 Interaction.question = staticmethod(question)
999 Interaction.information = staticmethod(information)