git-cola v2.11
[git-cola.git] / cola / qtutils.py
blobccf401f86523eb383cf2efaa4454c2b0951536a9
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 connect_released(button, fn):
53 """Connect a button to a function"""
54 button.released.connect(fn)
57 def button_action(button, action):
58 """Make a button trigger an action"""
59 connect_button(button, action.trigger)
62 def connect_toggle(toggle, fn):
63 """Connect a toggle button to a function"""
64 toggle.toggled.connect(fn)
67 def active_window():
68 """Return the active window for the current application"""
69 return QtWidgets.QApplication.activeWindow()
72 def hbox(margin, spacing, *items):
73 """Create an HBoxLayout with the specified sizes and items"""
74 return box(QtWidgets.QHBoxLayout, margin, spacing, *items)
77 def vbox(margin, spacing, *items):
78 """Create a VBoxLayout with the specified sizes and items"""
79 return box(QtWidgets.QVBoxLayout, margin, spacing, *items)
82 def buttongroup(*items):
83 """Create a QButtonGroup for the specified items"""
84 group = QtWidgets.QButtonGroup()
85 for i in items:
86 group.addButton(i)
87 return group
90 def set_margin(layout, margin):
91 """Set the content margins for a layout"""
92 layout.setContentsMargins(margin, margin, margin, margin)
95 def box(cls, margin, spacing, *items):
96 """Create a QBoxLayout with the specified sizes and items"""
97 stretch = STRETCH
98 skipped = SKIPPED
99 layout = cls()
100 layout.setSpacing(spacing)
101 set_margin(layout, margin)
103 for i in items:
104 if isinstance(i, QtWidgets.QWidget):
105 layout.addWidget(i)
106 elif isinstance(i, (QtWidgets.QHBoxLayout, QtWidgets.QVBoxLayout,
107 QtWidgets.QFormLayout, QtWidgets.QLayout)):
108 layout.addLayout(i)
109 elif i is stretch:
110 layout.addStretch()
111 elif i is skipped:
112 continue
113 elif isinstance(i, int_types):
114 layout.addSpacing(i)
116 return layout
119 def form(margin, spacing, *widgets):
120 """Create a QFormLayout with the specified sizes and items"""
121 layout = QtWidgets.QFormLayout()
122 layout.setSpacing(spacing)
123 layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
124 set_margin(layout, margin)
126 for idx, (name, widget) in enumerate(widgets):
127 if isinstance(name, (str, ustr)):
128 layout.addRow(name, widget)
129 else:
130 layout.setWidget(idx, QtWidgets.QFormLayout.LabelRole, name)
131 layout.setWidget(idx, QtWidgets.QFormLayout.FieldRole, widget)
133 return layout
136 def grid(margin, spacing, *widgets):
137 """Create a QGridLayout with the specified sizes and items"""
138 layout = QtWidgets.QGridLayout()
139 layout.setSpacing(spacing)
140 set_margin(layout, margin)
142 for row in widgets:
143 item = row[0]
144 if isinstance(item, QtWidgets.QWidget):
145 layout.addWidget(*row)
146 elif isinstance(item, QtWidgets.QLayoutItem):
147 layout.addItem(*row)
149 return layout
152 def splitter(orientation, *widgets):
153 """Create a spliter over the specified widgets
155 :param orientation: Qt.Horizontal or Qt.Vertical
158 layout = QtWidgets.QSplitter()
159 layout.setOrientation(orientation)
160 layout.setHandleWidth(defs.handle_width)
161 layout.setChildrenCollapsible(True)
163 for idx, widget in enumerate(widgets):
164 layout.addWidget(widget)
165 layout.setStretchFactor(idx, 1)
167 # Workaround for Qt not setting the WA_Hover property for QSplitter
168 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
169 layout.handle(1).setAttribute(Qt.WA_Hover)
171 return layout
174 def label(text=None, align=None, fmt=None, selectable=True):
175 """Create a QLabel with the specified properties"""
176 widget = QtWidgets.QLabel()
177 if align is not None:
178 widget.setAlignment(align)
179 if fmt is not None:
180 widget.setTextFormat(fmt)
181 if selectable:
182 widget.setTextInteractionFlags(Qt.TextBrowserInteraction)
183 widget.setOpenExternalLinks(True)
184 if text:
185 widget.setText(text)
186 return widget
189 def textbrowser(text=None):
190 """Create a QTextBrowser for the specified text"""
191 widget = QtWidgets.QTextBrowser()
192 widget.setOpenExternalLinks(True)
193 if text:
194 widget.setText(text)
195 return widget
198 def add_completer(widget, items):
199 """Add simple completion to a widget"""
200 completer = QtWidgets.QCompleter(items, widget)
201 completer.setCaseSensitivity(Qt.CaseInsensitive)
202 completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion)
203 widget.setCompleter(completer)
206 def prompt(msg, title=None, text=''):
207 """Presents the user with an input widget and returns the input."""
208 if title is None:
209 title = msg
210 result = QtWidgets.QInputDialog.getText(
211 active_window(), msg, title,
212 QtWidgets.QLineEdit.Normal, text)
213 return (result[0], result[1])
216 def prompt_n(msg, inputs):
217 """Presents the user with N input widgets and returns the results"""
218 dialog = QtWidgets.QDialog(active_window())
219 dialog.setWindowModality(Qt.WindowModal)
220 dialog.setWindowTitle(msg)
222 long_value = msg
223 for k, v in inputs:
224 if len(k + v) > len(long_value):
225 long_value = k + v
227 metrics = QtGui.QFontMetrics(dialog.font())
228 min_width = metrics.width(long_value) + 100
229 if min_width > 720:
230 min_width = 720
231 dialog.setMinimumWidth(min_width)
233 ok_b = ok_button(msg, enabled=False)
234 close_b = close_button()
236 form_widgets = []
238 def get_values():
239 return [pair[1].text().strip() for pair in form_widgets]
241 for name, value in inputs:
242 lineedit = QtWidgets.QLineEdit()
243 # Enable the OK button only when all fields have been populated
244 lineedit.textChanged.connect(
245 lambda x: ok_b.setEnabled(all(get_values())))
246 if value:
247 lineedit.setText(value)
248 form_widgets.append((name, lineedit))
250 # layouts
251 form_layout = form(defs.no_margin, defs.button_spacing, *form_widgets)
252 button_layout = hbox(defs.no_margin, defs.button_spacing,
253 STRETCH, close_b, ok_b)
254 main_layout = vbox(defs.margin, defs.button_spacing,
255 form_layout, button_layout)
256 dialog.setLayout(main_layout)
258 # connections
259 connect_button(ok_b, dialog.accept)
260 connect_button(close_b, dialog.reject)
262 accepted = dialog.exec_() == QtWidgets.QDialog.Accepted
263 text = get_values()
264 ok = accepted and all(text)
265 return (ok, text)
268 class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
270 TYPE = QtGui.QStandardItem.UserType + 101
272 def __init__(self, path, icon, deleted):
273 QtWidgets.QTreeWidgetItem.__init__(self)
274 self.path = path
275 self.deleted = deleted
276 self.setIcon(0, icons.from_name(icon))
277 self.setText(0, path)
279 def type(self):
280 return self.TYPE
283 def paths_from_indexes(model, indexes,
284 item_type=TreeWidgetItem.TYPE,
285 item_filter=None):
286 """Return paths from a list of QStandardItemModel indexes"""
287 items = [model.itemFromIndex(i) for i in indexes]
288 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
291 def _true_filter(x):
292 return True
295 def paths_from_items(items,
296 item_type=TreeWidgetItem.TYPE,
297 item_filter=None):
298 """Return a list of paths from a list of items"""
299 if item_filter is None:
300 item_filter = _true_filter
301 return [i.path for i in items
302 if i.type() == item_type and item_filter(i)]
305 def confirm(title, text, informative_text, ok_text,
306 icon=None, default=True,
307 cancel_text=None, cancel_icon=None):
308 """Confirm that an action should take place"""
309 msgbox = QtWidgets.QMessageBox(active_window())
310 msgbox.setWindowModality(Qt.WindowModal)
311 msgbox.setWindowTitle(title)
312 msgbox.setText(text)
313 msgbox.setInformativeText(informative_text)
315 icon = icons.mkicon(icon, icons.ok)
316 ok = msgbox.addButton(ok_text, QtWidgets.QMessageBox.ActionRole)
317 ok.setIcon(icon)
319 cancel = msgbox.addButton(QtWidgets.QMessageBox.Cancel)
320 cancel_icon = icons.mkicon(cancel_icon, icons.close)
321 cancel.setIcon(cancel_icon)
322 if cancel_text:
323 cancel.setText(cancel_text)
325 if default:
326 msgbox.setDefaultButton(ok)
327 else:
328 msgbox.setDefaultButton(cancel)
329 msgbox.exec_()
330 return msgbox.clickedButton() == ok
333 class ResizeableMessageBox(QtWidgets.QMessageBox):
335 def __init__(self, parent):
336 QtWidgets.QMessageBox.__init__(self, parent)
337 self.setMouseTracking(True)
338 self.setSizeGripEnabled(True)
340 def event(self, event):
341 res = QtWidgets.QMessageBox.event(self, event)
342 event_type = event.type()
343 if (event_type == QtCore.QEvent.MouseMove or
344 event_type == QtCore.QEvent.MouseButtonPress):
345 maxi = QtCore.QSize(defs.max_size, defs.max_size)
346 self.setMaximumSize(maxi)
347 text = self.findChild(QtWidgets.QTextEdit)
348 if text is not None:
349 expand = QtWidgets.QSizePolicy.Expanding
350 text.setSizePolicy(QtWidgets.QSizePolicy(expand, expand))
351 text.setMaximumSize(maxi)
352 return res
355 def critical(title, message=None, details=None):
356 """Show a warning with the provided title and message."""
357 if message is None:
358 message = title
359 mbox = ResizeableMessageBox(active_window())
360 mbox.setWindowTitle(title)
361 mbox.setTextFormat(Qt.PlainText)
362 mbox.setText(message)
363 mbox.setIcon(QtWidgets.QMessageBox.Critical)
364 mbox.setStandardButtons(QtWidgets.QMessageBox.Close)
365 mbox.setDefaultButton(QtWidgets.QMessageBox.Close)
366 if details:
367 mbox.setDetailedText(details)
368 mbox.exec_()
371 def information(title, message=None, details=None, informative_text=None):
372 """Show information with the provided title and message."""
373 if message is None:
374 message = title
375 mbox = QtWidgets.QMessageBox(active_window())
376 mbox.setStandardButtons(QtWidgets.QMessageBox.Close)
377 mbox.setDefaultButton(QtWidgets.QMessageBox.Close)
378 mbox.setWindowTitle(title)
379 mbox.setWindowModality(Qt.WindowModal)
380 mbox.setTextFormat(Qt.PlainText)
381 mbox.setText(message)
382 if informative_text:
383 mbox.setInformativeText(informative_text)
384 if details:
385 mbox.setDetailedText(details)
386 # Render into a 1-inch wide pixmap
387 pixmap = icons.cola().pixmap(defs.large_icon)
388 mbox.setIconPixmap(pixmap)
389 mbox.exec_()
392 def question(title, msg, default=True):
393 """Launches a QMessageBox question with the provided title and message.
394 Passing "default=False" will make "No" the default choice."""
395 yes = QtWidgets.QMessageBox.Yes
396 no = QtWidgets.QMessageBox.No
397 buttons = yes | no
398 if default:
399 default = yes
400 else:
401 default = no
403 parent = active_window()
404 MessageBox = QtWidgets.QMessageBox
405 result = MessageBox.question(parent, title, msg, buttons, default)
406 return result == QtWidgets.QMessageBox.Yes
409 def tree_selection(tree_item, items):
410 """Returns an array of model items that correspond to the selected
411 QTreeWidgetItem children"""
412 selected = []
413 count = min(tree_item.childCount(), len(items))
414 for idx in range(count):
415 if tree_item.child(idx).isSelected():
416 selected.append(items[idx])
418 return selected
421 def tree_selection_items(tree_item):
422 """Returns selected widget items"""
423 selected = []
424 for idx in range(tree_item.childCount()):
425 child = tree_item.child(idx)
426 if child.isSelected():
427 selected.append(child)
429 return selected
432 def selected_item(list_widget, items):
433 """Returns the model item that corresponds to the selected QListWidget
434 row."""
435 widget_items = list_widget.selectedItems()
436 if not widget_items:
437 return None
438 widget_item = widget_items[0]
439 row = list_widget.row(widget_item)
440 if row < len(items):
441 return items[row]
442 else:
443 return None
446 def selected_items(list_widget, items):
447 """Returns an array of model items that correspond to the selected
448 QListWidget rows."""
449 item_count = len(items)
450 selected = []
451 for widget_item in list_widget.selectedItems():
452 row = list_widget.row(widget_item)
453 if row < item_count:
454 selected.append(items[row])
455 return selected
458 def open_file(title, directory=None):
459 """Creates an Open File dialog and returns a filename."""
460 result = compat.getopenfilename(parent=active_window(),
461 caption=title,
462 basedir=directory)
463 return result[0]
466 def open_files(title, directory=None, filters=''):
467 """Creates an Open File dialog and returns a list of filenames."""
468 result = compat.getopenfilenames(parent=active_window(),
469 caption=title,
470 basedir=directory,
471 filters=filters)
472 return result[0]
475 def opendir_dialog(caption, path):
476 """Prompts for a directory path"""
478 options = (QtWidgets.QFileDialog.ShowDirsOnly |
479 QtWidgets.QFileDialog.DontResolveSymlinks)
480 return compat.getexistingdirectory(parent=active_window(),
481 caption=caption,
482 basedir=path,
483 options=options)
486 def save_as(filename, title='Save As...'):
487 """Creates a Save File dialog and returns a filename."""
488 result = compat.getsavefilename(parent=active_window(),
489 caption=title,
490 basedir=filename)
491 return result[0]
494 def copy_path(filename, absolute=True):
495 """Copy a filename to the clipboard"""
496 if filename is None:
497 return
498 if absolute:
499 filename = core.abspath(filename)
500 set_clipboard(filename)
503 def set_clipboard(text):
504 """Sets the copy/paste buffer to text."""
505 if not text:
506 return
507 clipboard = QtWidgets.QApplication.clipboard()
508 clipboard.setText(text, QtGui.QClipboard.Clipboard)
509 clipboard.setText(text, QtGui.QClipboard.Selection)
510 persist_clipboard()
513 def persist_clipboard():
514 """Persist the clipboard
516 X11 stores only a reference to the clipboard data.
517 Send a clipboard event to force a copy of the clipboard to occur.
518 This ensures that the clipboard is present after git-cola exits.
519 Otherwise, the reference is destroyed on exit.
521 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
524 clipboard = QtWidgets.QApplication.clipboard()
525 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
526 QtWidgets.QApplication.sendEvent(clipboard, event)
529 def add_action_bool(widget, text, fn, checked, *shortcuts):
530 tip = text
531 action = _add_action(widget, text, tip, fn, connect_action_bool, *shortcuts)
532 action.setCheckable(True)
533 action.setChecked(checked)
534 return action
537 def add_action(widget, text, fn, *shortcuts):
538 tip = text
539 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
542 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
543 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
546 def _add_action(widget, text, tip, fn, connect, *shortcuts):
547 action = QtWidgets.QAction(text, widget)
548 if hasattr(action, 'setIconVisibleInMenu'):
549 action.setIconVisibleInMenu(True)
550 if tip:
551 action.setStatusTip(tip)
552 connect(action, fn)
553 if shortcuts:
554 action.setShortcuts(shortcuts)
555 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
556 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
557 widget.addAction(action)
558 return action
561 def set_selected_item(widget, idx):
562 """Sets a the currently selected item to the item at index idx."""
563 if type(widget) is QtWidgets.QTreeWidget:
564 item = widget.topLevelItem(idx)
565 if item:
566 item.setSelected(True)
567 widget.setCurrentItem(item)
570 def add_items(widget, items):
571 """Adds items to a widget."""
572 for item in items:
573 if item is None:
574 continue
575 widget.addItem(item)
578 def set_items(widget, items):
579 """Clear the existing widget contents and set the new items."""
580 widget.clear()
581 add_items(widget, items)
584 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
585 """Given a filename, return a TreeWidgetItem for a status widget
587 "staged", "deleted, and "untracked" control which icon is used.
590 icon_name = icons.status(filename, deleted, staged, untracked)
591 return TreeWidgetItem(filename, icons.name_from_basename(icon_name),
592 deleted=deleted)
595 def add_close_action(widget):
596 """Adds close action and shortcuts to a widget."""
597 return add_action(widget, N_('Close...'),
598 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
601 def app():
602 """Return the current application"""
603 return QtWidgets.QApplication.instance()
606 def desktop():
607 """Return the desktop"""
608 return app().desktop()
611 def center_on_screen(widget):
612 """Move widget to the center of the default screen"""
613 desk = desktop()
614 rect = desk.screenGeometry(QtGui.QCursor().pos())
615 cy = rect.height()//2
616 cx = rect.width()//2
617 widget.move(cx - widget.width()//2, cy - widget.height()//2)
620 def default_size(parent, width, height, use_parent_height=True):
621 """Return the parent's size, or the provided defaults"""
622 if parent is not None:
623 width = parent.width()
624 if use_parent_height:
625 height = parent.height()
626 return (width, height)
629 def default_monospace_font():
630 font = QtGui.QFont()
631 family = 'Monospace'
632 if utils.is_darwin():
633 family = 'Monaco'
634 font.setFamily(family)
635 return font
638 def diff_font_str():
639 font_str = gitcfg.current().get(prefs.FONTDIFF)
640 if font_str is None:
641 font_str = default_monospace_font().toString()
642 return font_str
645 def diff_font():
646 return font(diff_font_str())
649 def font(string):
650 font = QtGui.QFont()
651 font.fromString(string)
652 return font
655 def create_button(text='', layout=None, tooltip=None, icon=None,
656 enabled=True, default=False):
657 """Create a button, set its title, and add it to the parent."""
658 button = QtWidgets.QPushButton()
659 button.setCursor(Qt.PointingHandCursor)
660 if text:
661 button.setText(text)
662 if icon is not None:
663 button.setIcon(icon)
664 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
665 if tooltip is not None:
666 button.setToolTip(tooltip)
667 if layout is not None:
668 layout.addWidget(button)
669 if not enabled:
670 button.setEnabled(False)
671 if default:
672 button.setDefault(True)
673 return button
676 def create_action_button(tooltip=None, icon=None):
677 button = QtWidgets.QPushButton()
678 button.setCursor(Qt.PointingHandCursor)
679 button.setFlat(True)
680 if tooltip is not None:
681 button.setToolTip(tooltip)
682 if icon is not None:
683 button.setIcon(icon)
684 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
685 return button
688 def ok_button(text, default=True, enabled=True, icon=None):
689 if icon is None:
690 icon = icons.ok()
691 return create_button(text=text, icon=icon, default=default, enabled=enabled)
694 def close_button():
695 return create_button(text=N_('Close'), icon=icons.close())
698 def edit_button(enabled=True, default=False):
699 return create_button(text=N_('Edit'), icon=icons.edit(),
700 enabled=enabled, default=default)
703 def refresh_button(enabled=True, default=False):
704 return create_button(text=N_('Refresh'), icon=icons.sync(),
705 enabled=enabled, default=default)
708 def hide_button_menu_indicator(button):
709 """Hide the menu indicator icon on buttons"""
711 name = button.__class__.__name__
712 stylesheet = """
713 %(name)s::menu-indicator {
714 image: none;
717 if name == 'QPushButton':
718 stylesheet += """
719 %(name)s {
720 border-style: none;
723 button.setStyleSheet(stylesheet % dict(name=name))
726 def checkbox(text='', tooltip='', checked=None):
727 """Create a checkbox"""
728 return _checkbox(QtWidgets.QCheckBox, text, tooltip, checked)
731 def radio(text='', tooltip='', checked=None):
732 """Create a radio button"""
733 return _checkbox(QtWidgets.QRadioButton, text, tooltip, checked)
736 def _checkbox(cls, text, tooltip, checked):
737 """Create a widget and apply properties"""
738 widget = cls()
739 if text:
740 widget.setText(text)
741 if tooltip:
742 widget.setToolTip(tooltip)
743 if checked is not None:
744 widget.setChecked(checked)
745 return widget
748 class DockTitleBarWidget(QtWidgets.QWidget):
750 def __init__(self, parent, title, stretch=True):
751 QtWidgets.QWidget.__init__(self, parent)
752 self.setAutoFillBackground(True)
753 self.label = qlabel = QtWidgets.QLabel(title, self)
754 font = qlabel.font()
755 font.setBold(True)
756 qlabel.setFont(font)
757 qlabel.setCursor(Qt.OpenHandCursor)
759 self.close_button = create_action_button(
760 tooltip=N_('Close'), icon=icons.close())
762 self.toggle_button = create_action_button(
763 tooltip=N_('Detach'), icon=icons.external())
765 self.corner_layout = hbox(defs.no_margin, defs.spacing)
767 if stretch:
768 separator = STRETCH
769 else:
770 separator = SKIPPED
772 self.main_layout = hbox(defs.small_margin, defs.spacing,
773 qlabel, separator, self.corner_layout,
774 self.toggle_button, self.close_button)
775 self.setLayout(self.main_layout)
777 connect_button(self.toggle_button, self.toggle_floating)
778 connect_button(self.close_button, self.toggle_visibility)
780 def toggle_floating(self):
781 self.parent().setFloating(not self.parent().isFloating())
782 self.update_tooltips()
784 def toggle_visibility(self):
785 self.parent().toggleViewAction().trigger()
787 def set_title(self, title):
788 self.label.setText(title)
790 def add_corner_widget(self, widget):
791 self.corner_layout.addWidget(widget)
793 def update_tooltips(self):
794 if self.parent().isFloating():
795 tooltip = N_('Attach')
796 else:
797 tooltip = N_('Detach')
798 self.toggle_button.setToolTip(tooltip)
801 def create_dock(title, parent, stretch=True):
802 """Create a dock widget and set it up accordingly."""
803 dock = QtWidgets.QDockWidget(parent)
804 dock.setWindowTitle(title)
805 dock.setObjectName(title)
806 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
807 dock.setTitleBarWidget(titlebar)
808 dock.setAutoFillBackground(True)
809 if hasattr(parent, 'dockwidgets'):
810 parent.dockwidgets.append(dock)
811 return dock
814 def create_menu(title, parent):
815 """Create a menu and set its title."""
816 qmenu = QtWidgets.QMenu(title, parent)
817 return qmenu
820 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
821 button = QtWidgets.QToolButton()
822 button.setAutoRaise(True)
823 button.setAutoFillBackground(True)
824 button.setCursor(Qt.PointingHandCursor)
825 if icon is not None:
826 button.setIcon(icon)
827 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
828 if text is not None:
829 button.setText(text)
830 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
831 if tooltip is not None:
832 button.setToolTip(tooltip)
833 if layout is not None:
834 layout.addWidget(button)
835 return button
838 def mimedata_from_paths(paths):
839 """Return mimedata with a list of absolute path URLs"""
841 abspaths = [core.abspath(path) for path in paths]
842 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
844 mimedata = QtCore.QMimeData()
845 mimedata.setUrls(urls)
847 # The text/x-moz-list format is always included by Qt, and doing
848 # mimedata.removeFormat('text/x-moz-url') has no effect.
849 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
851 # gnome-terminal expects utf-16 encoded text, but other terminals,
852 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
853 # to override the default.
854 paths_text = core.list2cmdline(abspaths)
855 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
856 moz_text = core.encode(paths_text, encoding=encoding)
857 mimedata.setData('text/x-moz-url', moz_text)
859 return mimedata
862 def path_mimetypes():
863 return ['text/uri-list', 'text/x-moz-url']
866 class BlockSignals(object):
867 """Context manager for blocking a signals on a widget"""
869 def __init__(self, *widgets):
870 self.widgets = widgets
871 self.values = {}
873 def __enter__(self):
874 for w in self.widgets:
875 self.values[w] = w.blockSignals(True)
876 return self
878 def __exit__(self, exc_type, exc_val, exc_tb):
879 for w in self.widgets:
880 w.blockSignals(self.values[w])
883 class Channel(QtCore.QObject):
884 finished = Signal(object)
885 result = Signal(object)
888 class Task(QtCore.QRunnable):
889 """Disable auto-deletion to avoid gc issues
891 Python's garbage collector will try to double-free the task
892 once it's finished, so disable Qt's auto-deletion as a workaround.
896 def __init__(self, parent):
897 QtCore.QRunnable.__init__(self)
899 self.channel = Channel()
900 self.result = None
901 self.setAutoDelete(False)
903 def run(self):
904 self.result = self.task()
905 self.channel.result.emit(self.result)
906 self.done()
908 def task(self):
909 return None
911 def done(self):
912 self.channel.finished.emit(self)
914 def connect(self, handler):
915 self.channel.result.connect(handler, type=Qt.QueuedConnection)
918 class SimpleTask(Task):
919 """Run a simple callable as a task"""
921 def __init__(self, parent, fn, *args, **kwargs):
922 Task.__init__(self, parent)
924 self.fn = fn
925 self.args = args
926 self.kwargs = kwargs
928 def task(self):
929 return self.fn(*self.args, **self.kwargs)
932 class RunTask(QtCore.QObject):
933 """Runs QRunnable instances and transfers control when they finish"""
935 def __init__(self, parent=None):
936 QtCore.QObject.__init__(self, parent)
937 self.tasks = []
938 self.task_details = {}
939 self.threadpool = QtCore.QThreadPool.globalInstance()
941 def start(self, task, progress=None, finish=None):
942 """Start the task and register a callback"""
943 if progress is not None:
944 progress.show()
945 # prevents garbage collection bugs in certain PyQt4 versions
946 self.tasks.append(task)
947 task_id = id(task)
948 self.task_details[task_id] = (progress, finish)
950 task.channel.finished.connect(self.finish, type=Qt.QueuedConnection)
951 self.threadpool.start(task)
953 def finish(self, task):
954 task_id = id(task)
955 try:
956 self.tasks.remove(task)
957 except:
958 pass
959 try:
960 progress, finish = self.task_details[task_id]
961 del self.task_details[task_id]
962 except KeyError:
963 finish = progress = None
965 if progress is not None:
966 progress.hide()
968 if finish is not None:
969 finish(task)
972 # Syntax highlighting
974 def rgb(r, g, b):
975 color = QtGui.QColor()
976 color.setRgb(r, g, b)
977 return color
980 def rgba(r, g, b, a=255):
981 color = rgb(r, g, b)
982 color.setAlpha(a)
983 return color
986 def RGB(args):
987 return rgb(*args)
990 def rgb_css(color):
991 """Convert a QColor into an rgb(int, int, int) CSS string"""
992 return 'rgb(%d, %d, %d)' % (color.red(), color.green(), color.blue())
995 def rgb_hex(color):
996 """Convert a QColor into a hex aabbcc string"""
997 return '%x%x%x' % (color.red(), color.green(), color.blue())
1000 def make_format(fg=None, bg=None, bold=False):
1001 fmt = QtGui.QTextCharFormat()
1002 if fg:
1003 fmt.setForeground(fg)
1004 if bg:
1005 fmt.setBackground(bg)
1006 if bold:
1007 fmt.setFontWeight(QtGui.QFont.Bold)
1008 return fmt
1011 def install():
1012 Interaction.critical = staticmethod(critical)
1013 Interaction.confirm = staticmethod(confirm)
1014 Interaction.question = staticmethod(question)
1015 Interaction.information = staticmethod(information)