cmds: provide $DIRNAME in the environment for guitool commands
[git-cola.git] / cola / qtutils.py
blobb930f679243a4c29e2f53fc1f89183a522e2336d
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 connect_action(action, fn):
30 """Connect an action to a function"""
31 action.triggered[bool].connect(lambda x: fn())
34 def connect_action_bool(action, fn):
35 """Connect a triggered(bool) action to a function"""
36 action.triggered[bool].connect(fn)
39 def connect_button(button, fn):
40 """Connect a button to a function"""
41 button.pressed.connect(fn)
44 def button_action(button, action):
45 """Make a button trigger an action"""
46 connect_button(button, action.trigger)
49 def connect_toggle(toggle, fn):
50 """Connect a toggle button to a function"""
51 toggle.toggled.connect(fn)
54 def active_window():
55 """Return the active window for the current application"""
56 return QtWidgets.QApplication.activeWindow()
59 def hbox(margin, spacing, *items):
60 """Create an HBoxLayout with the specified sizes and items"""
61 return box(QtWidgets.QHBoxLayout, margin, spacing, *items)
64 def vbox(margin, spacing, *items):
65 """Create a VBoxLayout with the specified sizes and items"""
66 return box(QtWidgets.QVBoxLayout, margin, spacing, *items)
69 def buttongroup(*items):
70 """Create a QButtonGroup for the specified items"""
71 group = QtWidgets.QButtonGroup()
72 for i in items:
73 group.addButton(i)
74 return group
77 def set_margin(layout, margin):
78 """Set the content margins for a layout"""
79 layout.setContentsMargins(margin, margin, margin, margin)
82 def box(cls, margin, spacing, *items):
83 """Create a QBoxLayout with the specified sizes and items"""
84 stretch = STRETCH
85 skipped = SKIPPED
86 layout = cls()
87 layout.setSpacing(spacing)
88 set_margin(layout, margin)
90 for i in items:
91 if isinstance(i, QtWidgets.QWidget):
92 layout.addWidget(i)
93 elif isinstance(i, (QtWidgets.QHBoxLayout, QtWidgets.QVBoxLayout,
94 QtWidgets.QFormLayout, QtWidgets.QLayout)):
95 layout.addLayout(i)
96 elif i is stretch:
97 layout.addStretch()
98 elif i is skipped:
99 continue
100 elif isinstance(i, int_types):
101 layout.addSpacing(i)
103 return layout
106 def form(margin, spacing, *widgets):
107 """Create a QFormLayout with the specified sizes and items"""
108 layout = QtWidgets.QFormLayout()
109 layout.setSpacing(spacing)
110 layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
111 set_margin(layout, margin)
113 for idx, (name, widget) in enumerate(widgets):
114 if isinstance(name, (str, ustr)):
115 layout.addRow(name, widget)
116 else:
117 layout.setWidget(idx, QtWidgets.QFormLayout.LabelRole, name)
118 layout.setWidget(idx, QtWidgets.QFormLayout.FieldRole, widget)
120 return layout
123 def grid(margin, spacing, *widgets):
124 """Create a QGridLayout with the specified sizes and items"""
125 layout = QtWidgets.QGridLayout()
126 layout.setSpacing(spacing)
127 set_margin(layout, margin)
129 for row in widgets:
130 item = row[0]
131 if isinstance(item, QtWidgets.QWidget):
132 layout.addWidget(*row)
133 elif isinstance(item, QtWidgets.QLayoutItem):
134 layout.addItem(*row)
136 return layout
139 def splitter(orientation, *widgets):
140 """Create a spliter over the specified widgets
142 :param orientation: Qt.Horizontal or Qt.Vertical
145 layout = QtWidgets.QSplitter()
146 layout.setOrientation(orientation)
147 layout.setHandleWidth(defs.handle_width)
148 layout.setChildrenCollapsible(True)
149 for idx, widget in enumerate(widgets):
150 layout.addWidget(widget)
151 layout.setStretchFactor(idx, 1)
153 return layout
156 def label(text=None, align=None, fmt=None, selectable=True, stylesheet=None):
157 """Create a QLabel with the specified properties"""
158 widget = QtWidgets.QLabel()
159 if stylesheet:
160 widget.setStyleSheet(stylesheet)
161 if align is not None:
162 widget.setAlignment(align)
163 if fmt is not None:
164 widget.setTextFormat(fmt)
165 if selectable:
166 widget.setTextInteractionFlags(Qt.TextBrowserInteraction)
167 widget.setOpenExternalLinks(True)
168 if text:
169 widget.setText(text)
170 return widget
173 def textbrowser(text=None):
174 """Create a QTextBrowser for the specified text"""
175 widget = QtWidgets.QTextBrowser()
176 widget.setOpenExternalLinks(True)
177 if text:
178 widget.setText(text)
179 return widget
182 def prompt(msg, title=None, text=''):
183 """Presents the user with an input widget and returns the input."""
184 if title is None:
185 title = msg
186 result = QtWidgets.QInputDialog.getText(
187 active_window(), msg, title,
188 QtWidgets.QLineEdit.Normal, text)
189 return (result[0], result[1])
192 def prompt_n(msg, inputs):
193 """Presents the user with N input widgets and returns the results"""
194 dialog = QtWidgets.QDialog(active_window())
195 dialog.setWindowModality(Qt.WindowModal)
196 dialog.setWindowTitle(msg)
198 long_value = msg
199 for k, v in inputs:
200 if len(k + v) > len(long_value):
201 long_value = k + v
203 metrics = QtGui.QFontMetrics(dialog.font())
204 min_width = metrics.width(long_value) + 100
205 if min_width > 720:
206 min_width = 720
207 dialog.setMinimumWidth(min_width)
209 ok_b = ok_button(msg, enabled=False)
210 close_b = close_button()
212 form_widgets = []
214 def get_values():
215 return [pair[1].text().strip() for pair in form_widgets]
217 for name, value in inputs:
218 lineedit = QtWidgets.QLineEdit()
219 # Enable the OK button only when all fields have been populated
220 lineedit.textChanged.connect(
221 lambda x: ok_b.setEnabled(all(get_values())))
222 if value:
223 lineedit.setText(value)
224 form_widgets.append((name, lineedit))
226 # layouts
227 form_layout = form(defs.no_margin, defs.button_spacing, *form_widgets)
228 button_layout = hbox(defs.no_margin, defs.button_spacing,
229 STRETCH, close_b, ok_b)
230 main_layout = vbox(defs.margin, defs.button_spacing,
231 form_layout, button_layout)
232 dialog.setLayout(main_layout)
234 # connections
235 connect_button(ok_b, dialog.accept)
236 connect_button(close_b, dialog.reject)
238 accepted = dialog.exec_() == QtWidgets.QDialog.Accepted
239 text = get_values()
240 ok = accepted and all(text)
241 return (ok, text)
244 class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
246 TYPE = QtGui.QStandardItem.UserType + 101
248 def __init__(self, path, icon, deleted):
249 QtWidgets.QTreeWidgetItem.__init__(self)
250 self.path = path
251 self.deleted = deleted
252 self.setIcon(0, icons.from_name(icon))
253 self.setText(0, path)
255 def type(self):
256 return self.TYPE
259 def paths_from_indexes(model, indexes,
260 item_type=TreeWidgetItem.TYPE,
261 item_filter=None):
262 """Return paths from a list of QStandardItemModel indexes"""
263 items = [model.itemFromIndex(i) for i in indexes]
264 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
267 def _true_filter(x):
268 return True
271 def paths_from_items(items,
272 item_type=TreeWidgetItem.TYPE,
273 item_filter=None):
274 """Return a list of paths from a list of items"""
275 if item_filter is None:
276 item_filter = _true_filter
277 return [i.path for i in items
278 if i.type() == item_type and item_filter(i)]
281 def confirm(title, text, informative_text, ok_text,
282 icon=None, default=True,
283 cancel_text=None, cancel_icon=None):
284 """Confirm that an action should take place"""
285 msgbox = QtWidgets.QMessageBox(active_window())
286 msgbox.setWindowModality(Qt.WindowModal)
287 msgbox.setWindowTitle(title)
288 msgbox.setText(text)
289 msgbox.setInformativeText(informative_text)
291 icon = icons.mkicon(icon, icons.ok)
292 ok = msgbox.addButton(ok_text, QtWidgets.QMessageBox.ActionRole)
293 ok.setIcon(icon)
295 cancel = msgbox.addButton(QtWidgets.QMessageBox.Cancel)
296 cancel_icon = icons.mkicon(cancel_icon, icons.close)
297 cancel.setIcon(cancel_icon)
298 if cancel_text:
299 cancel.setText(cancel_text)
301 if default:
302 msgbox.setDefaultButton(ok)
303 else:
304 msgbox.setDefaultButton(cancel)
305 msgbox.exec_()
306 return msgbox.clickedButton() == ok
309 class ResizeableMessageBox(QtWidgets.QMessageBox):
311 def __init__(self, parent):
312 QtWidgets.QMessageBox.__init__(self, parent)
313 self.setMouseTracking(True)
314 self.setSizeGripEnabled(True)
316 def event(self, event):
317 res = QtWidgets.QMessageBox.event(self, event)
318 event_type = event.type()
319 if (event_type == QtCore.QEvent.MouseMove or
320 event_type == QtCore.QEvent.MouseButtonPress):
321 maxi = QtCore.QSize(defs.max_size, defs.max_size)
322 self.setMaximumSize(maxi)
323 text = self.findChild(QtWidgets.QTextEdit)
324 if text is not None:
325 expand = QtWidgets.QSizePolicy.Expanding
326 text.setSizePolicy(QtWidgets.QSizePolicy(expand, expand))
327 text.setMaximumSize(maxi)
328 return res
331 def critical(title, message=None, details=None):
332 """Show a warning with the provided title and message."""
333 if message is None:
334 message = title
335 mbox = ResizeableMessageBox(active_window())
336 mbox.setWindowTitle(title)
337 mbox.setTextFormat(Qt.PlainText)
338 mbox.setText(message)
339 mbox.setIcon(QtWidgets.QMessageBox.Critical)
340 mbox.setStandardButtons(QtWidgets.QMessageBox.Close)
341 mbox.setDefaultButton(QtWidgets.QMessageBox.Close)
342 if details:
343 mbox.setDetailedText(details)
344 mbox.exec_()
347 def information(title, message=None, details=None, informative_text=None):
348 """Show information with the provided title and message."""
349 if message is None:
350 message = title
351 mbox = QtWidgets.QMessageBox(active_window())
352 mbox.setStandardButtons(QtWidgets.QMessageBox.Close)
353 mbox.setDefaultButton(QtWidgets.QMessageBox.Close)
354 mbox.setWindowTitle(title)
355 mbox.setWindowModality(Qt.WindowModal)
356 mbox.setTextFormat(Qt.PlainText)
357 mbox.setText(message)
358 if informative_text:
359 mbox.setInformativeText(informative_text)
360 if details:
361 mbox.setDetailedText(details)
362 # Render into a 1-inch wide pixmap
363 pixmap = icons.cola().pixmap(defs.large_icon)
364 mbox.setIconPixmap(pixmap)
365 mbox.exec_()
368 def question(title, msg, default=True):
369 """Launches a QMessageBox question with the provided title and message.
370 Passing "default=False" will make "No" the default choice."""
371 yes = QtWidgets.QMessageBox.Yes
372 no = QtWidgets.QMessageBox.No
373 buttons = yes | no
374 if default:
375 default = yes
376 else:
377 default = no
379 parent = active_window()
380 MessageBox = QtWidgets.QMessageBox
381 result = MessageBox.question(parent, title, msg, buttons, default)
382 return result == QtWidgets.QMessageBox.Yes
385 def tree_selection(tree_item, items):
386 """Returns an array of model items that correspond to the selected
387 QTreeWidgetItem children"""
388 selected = []
389 count = min(tree_item.childCount(), len(items))
390 for idx in range(count):
391 if tree_item.child(idx).isSelected():
392 selected.append(items[idx])
394 return selected
397 def tree_selection_items(tree_item):
398 """Returns selected widget items"""
399 selected = []
400 for idx in range(tree_item.childCount()):
401 child = tree_item.child(idx)
402 if child.isSelected():
403 selected.append(child)
405 return selected
408 def selected_item(list_widget, items):
409 """Returns the model item that corresponds to the selected QListWidget
410 row."""
411 widget_items = list_widget.selectedItems()
412 if not widget_items:
413 return None
414 widget_item = widget_items[0]
415 row = list_widget.row(widget_item)
416 if row < len(items):
417 return items[row]
418 else:
419 return None
422 def selected_items(list_widget, items):
423 """Returns an array of model items that correspond to the selected
424 QListWidget rows."""
425 item_count = len(items)
426 selected = []
427 for widget_item in list_widget.selectedItems():
428 row = list_widget.row(widget_item)
429 if row < item_count:
430 selected.append(items[row])
431 return selected
434 def open_file(title, directory=None):
435 """Creates an Open File dialog and returns a filename."""
436 result = compat.getopenfilename(parent=active_window(),
437 caption=title,
438 basedir=directory)
439 return result[0]
442 def open_files(title, directory=None, filters=''):
443 """Creates an Open File dialog and returns a list of filenames."""
444 result = compat.getopenfilenames(parent=active_window(),
445 caption=title,
446 basedir=directory,
447 filters=filters)
448 return result[0]
451 def opendir_dialog(caption, path):
452 """Prompts for a directory path"""
454 options = (QtWidgets.QFileDialog.ShowDirsOnly |
455 QtWidgets.QFileDialog.DontResolveSymlinks)
456 return compat.getexistingdirectory(parent=active_window(),
457 caption=caption,
458 basedir=path,
459 options=options)
462 def save_as(filename, title='Save As...'):
463 """Creates a Save File dialog and returns a filename."""
464 result = compat.getsavefilename(parent=active_window(),
465 caption=title,
466 basedir=filename)
467 return result[0]
470 def copy_path(filename, absolute=True):
471 """Copy a filename to the clipboard"""
472 if filename is None:
473 return
474 if absolute:
475 filename = core.abspath(filename)
476 set_clipboard(filename)
479 def set_clipboard(text):
480 """Sets the copy/paste buffer to text."""
481 if not text:
482 return
483 clipboard = QtWidgets.QApplication.clipboard()
484 clipboard.setText(text, QtGui.QClipboard.Clipboard)
485 clipboard.setText(text, QtGui.QClipboard.Selection)
486 persist_clipboard()
489 def persist_clipboard():
490 """Persist the clipboard
492 X11 stores only a reference to the clipboard data.
493 Send a clipboard event to force a copy of the clipboard to occur.
494 This ensures that the clipboard is present after git-cola exits.
495 Otherwise, the reference is destroyed on exit.
497 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
500 clipboard = QtWidgets.QApplication.clipboard()
501 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
502 QtWidgets.QApplication.sendEvent(clipboard, event)
505 def add_action_bool(widget, text, fn, checked, *shortcuts):
506 tip = text
507 action = _add_action(widget, text, tip, fn, connect_action_bool, *shortcuts)
508 action.setCheckable(True)
509 action.setChecked(checked)
510 return action
513 def add_action(widget, text, fn, *shortcuts):
514 tip = text
515 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
518 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
519 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
522 def _add_action(widget, text, tip, fn, connect, *shortcuts):
523 action = QtWidgets.QAction(text, widget)
524 if hasattr(action, 'setIconVisibleInMenu'):
525 action.setIconVisibleInMenu(True)
526 if tip:
527 action.setStatusTip(tip)
528 connect(action, fn)
529 if shortcuts:
530 action.setShortcuts(shortcuts)
531 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
532 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
533 widget.addAction(action)
534 return action
537 def set_selected_item(widget, idx):
538 """Sets a the currently selected item to the item at index idx."""
539 if type(widget) is QtWidgets.QTreeWidget:
540 item = widget.topLevelItem(idx)
541 if item:
542 item.setSelected(True)
543 widget.setCurrentItem(item)
546 def add_items(widget, items):
547 """Adds items to a widget."""
548 for item in items:
549 if item is None:
550 continue
551 widget.addItem(item)
554 def set_items(widget, items):
555 """Clear the existing widget contents and set the new items."""
556 widget.clear()
557 add_items(widget, items)
560 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
561 """Given a filename, return a TreeWidgetItem for a status widget
563 "staged", "deleted, and "untracked" control which icon is used.
566 icon_name = icons.status(filename, deleted, staged, untracked)
567 return TreeWidgetItem(filename, icons.name_from_basename(icon_name),
568 deleted=deleted)
571 def add_close_action(widget):
572 """Adds close action and shortcuts to a widget."""
573 return add_action(widget, N_('Close...'),
574 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
577 def desktop():
578 return QtWidgets.QApplication.instance().desktop()
581 def center_on_screen(widget):
582 """Move widget to the center of the default screen"""
583 desk = desktop()
584 rect = desk.screenGeometry(QtGui.QCursor().pos())
585 cy = rect.height()//2
586 cx = rect.width()//2
587 widget.move(cx - widget.width()//2, cy - widget.height()//2)
590 def default_size(parent, width, height):
591 """Return the parent's size, or the provided defaults"""
592 if parent is not None:
593 width = parent.width()
594 height = parent.height()
595 return (width, height)
598 def default_monospace_font():
599 font = QtGui.QFont()
600 family = 'Monospace'
601 if utils.is_darwin():
602 family = 'Monaco'
603 font.setFamily(family)
604 return font
607 def diff_font_str():
608 font_str = gitcfg.current().get(prefs.FONTDIFF)
609 if font_str is None:
610 font_str = default_monospace_font().toString()
611 return font_str
614 def diff_font():
615 return font(diff_font_str())
618 def font(string):
619 font = QtGui.QFont()
620 font.fromString(string)
621 return font
624 def create_button(text='', layout=None, tooltip=None, icon=None,
625 enabled=True, default=False):
626 """Create a button, set its title, and add it to the parent."""
627 button = QtWidgets.QPushButton()
628 button.setCursor(Qt.PointingHandCursor)
629 if text:
630 button.setText(text)
631 if icon is not None:
632 button.setIcon(icon)
633 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
634 if tooltip is not None:
635 button.setToolTip(tooltip)
636 if layout is not None:
637 layout.addWidget(button)
638 if not enabled:
639 button.setEnabled(False)
640 if default:
641 button.setDefault(True)
642 return button
645 def create_action_button(tooltip=None, icon=None):
646 button = QtWidgets.QPushButton()
647 button.setCursor(Qt.PointingHandCursor)
648 button.setFlat(True)
649 if tooltip is not None:
650 button.setToolTip(tooltip)
651 if icon is not None:
652 button.setIcon(icon)
653 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
654 return button
657 def ok_button(text, default=True, enabled=True, icon=None):
658 if icon is None:
659 icon = icons.ok()
660 return create_button(text=text, icon=icon, default=default, enabled=enabled)
663 def close_button():
664 return create_button(text=N_('Close'), icon=icons.close())
667 def edit_button(enabled=True, default=False):
668 return create_button(text=N_('Edit'), icon=icons.edit(),
669 enabled=enabled, default=default)
672 def refresh_button(enabled=True, default=False):
673 return create_button(text=N_('Refresh'), icon=icons.sync(),
674 enabled=enabled, default=default)
677 def hide_button_menu_indicator(button):
678 cls = type(button)
679 name = cls.__name__
680 stylesheet = """
681 %(name)s::menu-indicator {
682 image: none;
685 if name == 'QPushButton':
686 stylesheet += """
687 %(name)s {
688 border-style: none;
691 button.setStyleSheet(stylesheet % {'name': name})
694 def checkbox(text='', tooltip='', checked=None):
695 cb = QtWidgets.QCheckBox()
696 if text:
697 cb.setText(text)
698 if tooltip:
699 cb.setToolTip(tooltip)
700 if checked is not None:
701 cb.setChecked(checked)
703 url = icons.check_name()
704 style = """
705 QCheckBox::indicator {
706 width: %(size)dpx;
707 height: %(size)dpx;
709 QCheckBox::indicator::unchecked {
710 border: %(border)dpx solid #999;
711 background: #fff;
713 QCheckBox::indicator::checked {
714 image: url(%(url)s);
715 border: %(border)dpx solid black;
716 background: #fff;
718 """ % dict(size=defs.checkbox, border=defs.border, url=url)
719 cb.setStyleSheet(style)
721 return cb
724 def radio(text='', tooltip='', checked=None):
725 rb = QtWidgets.QRadioButton()
726 if text:
727 rb.setText(text)
728 if tooltip:
729 rb.setToolTip(tooltip)
730 if checked is not None:
731 rb.setChecked(checked)
733 size = defs.checkbox
734 radius = size / 2
735 border = defs.radio_border
736 url = icons.dot_name()
737 style = """
738 QRadioButton::indicator {
739 width: %(size)dpx;
740 height: %(size)dpx;
742 QRadioButton::indicator::unchecked {
743 background: #fff;
744 border: %(border)dpx solid #999;
745 border-radius: %(radius)dpx;
747 QRadioButton::indicator::checked {
748 image: url(%(url)s);
749 background: #fff;
750 border: %(border)dpx solid black;
751 border-radius: %(radius)dpx;
753 """ % dict(size=size, radius=radius, border=border, url=url)
754 rb.setStyleSheet(style)
756 return rb
759 class DockTitleBarWidget(QtWidgets.QWidget):
761 def __init__(self, parent, title, stretch=True):
762 QtWidgets.QWidget.__init__(self, parent)
763 self.label = qlabel = QtWidgets.QLabel()
764 font = qlabel.font()
765 font.setBold(True)
766 qlabel.setFont(font)
767 qlabel.setText(title)
768 qlabel.setCursor(Qt.OpenHandCursor)
770 self.close_button = create_action_button(
771 tooltip=N_('Close'), icon=icons.close())
773 self.toggle_button = create_action_button(
774 tooltip=N_('Detach'), icon=icons.external())
776 self.corner_layout = hbox(defs.no_margin, defs.spacing)
778 if stretch:
779 separator = STRETCH
780 else:
781 separator = SKIPPED
783 self.main_layout = hbox(defs.small_margin, defs.spacing,
784 qlabel, separator, self.corner_layout,
785 self.toggle_button, self.close_button)
786 self.setLayout(self.main_layout)
788 connect_button(self.toggle_button, self.toggle_floating)
789 connect_button(self.close_button, self.toggle_visibility)
791 def toggle_floating(self):
792 self.parent().setFloating(not self.parent().isFloating())
793 self.update_tooltips()
795 def toggle_visibility(self):
796 self.parent().toggleViewAction().trigger()
798 def set_title(self, title):
799 self.label.setText(title)
801 def add_corner_widget(self, widget):
802 self.corner_layout.addWidget(widget)
804 def update_tooltips(self):
805 if self.parent().isFloating():
806 tooltip = N_('Attach')
807 else:
808 tooltip = N_('Detach')
809 self.toggle_button.setToolTip(tooltip)
812 def create_dock(title, parent, stretch=True):
813 """Create a dock widget and set it up accordingly."""
814 dock = QtWidgets.QDockWidget(parent)
815 dock.setWindowTitle(title)
816 dock.setObjectName(title)
817 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
818 dock.setTitleBarWidget(titlebar)
819 if hasattr(parent, 'dockwidgets'):
820 parent.dockwidgets.append(dock)
821 return dock
824 def create_menu(title, parent):
825 """Create a menu and set its title."""
826 qmenu = QtWidgets.QMenu(title, parent)
827 return qmenu
830 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
831 button = QtWidgets.QToolButton()
832 button.setAutoRaise(True)
833 button.setAutoFillBackground(True)
834 button.setCursor(Qt.PointingHandCursor)
835 if icon is not None:
836 button.setIcon(icon)
837 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
838 if text is not None:
839 button.setText(text)
840 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
841 if tooltip is not None:
842 button.setToolTip(tooltip)
843 if layout is not None:
844 layout.addWidget(button)
845 return button
848 def mimedata_from_paths(paths):
849 """Return mimedata with a list of absolute path URLs"""
851 abspaths = [core.abspath(path) for path in paths]
852 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
854 mimedata = QtCore.QMimeData()
855 mimedata.setUrls(urls)
857 # The text/x-moz-list format is always included by Qt, and doing
858 # mimedata.removeFormat('text/x-moz-url') has no effect.
859 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
861 # gnome-terminal expects utf-16 encoded text, but other terminals,
862 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
863 # to override the default.
864 paths_text = core.list2cmdline(abspaths)
865 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
866 moz_text = core.encode(paths_text, encoding=encoding)
867 mimedata.setData('text/x-moz-url', moz_text)
869 return mimedata
872 def path_mimetypes():
873 return ['text/uri-list', 'text/x-moz-url']
876 class BlockSignals(object):
877 """Context manager for blocking a signals on a widget"""
879 def __init__(self, *widgets):
880 self.widgets = widgets
881 self.values = {}
883 def __enter__(self):
884 for w in self.widgets:
885 self.values[w] = w.blockSignals(True)
886 return self
888 def __exit__(self, exc_type, exc_val, exc_tb):
889 for w in self.widgets:
890 w.blockSignals(self.values[w])
893 class Channel(QtCore.QObject):
894 finished = Signal(object)
895 result = Signal(object)
898 class Task(QtCore.QRunnable):
899 """Disable auto-deletion to avoid gc issues
901 Python's garbage collector will try to double-free the task
902 once it's finished, so disable Qt's auto-deletion as a workaround.
906 def __init__(self, parent):
907 QtCore.QRunnable.__init__(self)
909 self.channel = Channel()
910 self.result = None
911 self.setAutoDelete(False)
913 def run(self):
914 self.result = self.task()
915 self.channel.result.emit(self.result)
916 self.done()
918 def task(self):
919 return None
921 def done(self):
922 self.channel.finished.emit(self)
924 def connect(self, handler):
925 self.channel.result.connect(handler, type=Qt.QueuedConnection)
928 class SimpleTask(Task):
929 """Run a simple callable as a task"""
931 def __init__(self, parent, fn, *args, **kwargs):
932 Task.__init__(self, parent)
934 self.fn = fn
935 self.args = args
936 self.kwargs = kwargs
938 def task(self):
939 return self.fn(*self.args, **self.kwargs)
942 class RunTask(QtCore.QObject):
943 """Runs QRunnable instances and transfers control when they finish"""
945 def __init__(self, parent=None):
946 QtCore.QObject.__init__(self, parent)
947 self.tasks = []
948 self.task_details = {}
949 self.threadpool = QtCore.QThreadPool.globalInstance()
951 def start(self, task, progress=None, finish=None):
952 """Start the task and register a callback"""
953 if progress is not None:
954 progress.show()
955 # prevents garbage collection bugs in certain PyQt4 versions
956 self.tasks.append(task)
957 task_id = id(task)
958 self.task_details[task_id] = (progress, finish)
960 task.channel.finished.connect(self.finish, type=Qt.QueuedConnection)
961 self.threadpool.start(task)
963 def finish(self, task):
964 task_id = id(task)
965 try:
966 self.tasks.remove(task)
967 except:
968 pass
969 try:
970 progress, finish = self.task_details[task_id]
971 del self.task_details[task_id]
972 except KeyError:
973 finish = progress = None
975 if progress is not None:
976 progress.hide()
978 if finish is not None:
979 finish(task)
982 # Syntax highlighting
984 def rgba(r, g, b, a=255):
985 c = QtGui.QColor()
986 c.setRgb(r, g, b)
987 c.setAlpha(a)
988 return c
991 def RGB(args):
992 return rgba(*args)
995 def make_format(fg=None, bg=None, bold=False):
996 fmt = QtGui.QTextCharFormat()
997 if fg:
998 fmt.setForeground(fg)
999 if bg:
1000 fmt.setBackground(bg)
1001 if bold:
1002 fmt.setFontWeight(QtGui.QFont.Bold)
1003 return fmt
1006 def install():
1007 Interaction.critical = staticmethod(critical)
1008 Interaction.confirm = staticmethod(confirm)
1009 Interaction.question = staticmethod(question)
1010 Interaction.information = staticmethod(information)