doc: update v3.3 release notes draft
[git-cola.git] / cola / qtutils.py
blobdf42c2b307a7a8e960901dc519732e0169519d50
1 # Copyright (C) 2007-2018 David Aguilar and contributors
2 """Miscellaneous Qt utility functions."""
3 from __future__ import division, absolute_import, unicode_literals
4 import os
6 from qtpy import compat
7 from qtpy import QtGui
8 from qtpy import QtCore
9 from qtpy import QtWidgets
10 from qtpy.QtCore import Qt
11 from qtpy.QtCore import Signal
13 from . import core
14 from . import hotkeys
15 from . import icons
16 from . import utils
17 from .i18n import N_
18 from .compat import int_types
19 from .compat import ustr
20 from .models import prefs
21 from .widgets import defs
24 STRETCH = object()
25 SKIPPED = object()
28 def active_window():
29 """Return the active window for the current application"""
30 return QtWidgets.QApplication.activeWindow()
33 def connect_action(action, fn):
34 """Connect an action to a function"""
35 action.triggered[bool].connect(lambda x: fn())
38 def connect_action_bool(action, fn):
39 """Connect a triggered(bool) action to a function"""
40 action.triggered[bool].connect(fn)
43 def connect_button(button, fn):
44 """Connect a button to a function"""
45 # Some versions of Qt send the `bool` argument to the clicked callback,
46 # and some do not. The lambda consumes all callback-provided arguments.
47 button.clicked.connect(lambda *args, **kwargs: fn())
50 def connect_checkbox(widget, fn):
51 """Connect a checkbox to a function taking bool"""
52 widget.clicked.connect(lambda *args, **kwargs: fn(get(checkbox)))
55 def connect_released(button, fn):
56 """Connect a button to a function"""
57 button.released.connect(fn)
60 def button_action(button, action):
61 """Make a button trigger an action"""
62 connect_button(button, action.trigger)
65 def connect_toggle(toggle, fn):
66 """Connect a toggle button to a function"""
67 toggle.toggled.connect(fn)
70 def disconnect(signal):
71 """Disconnect signal from all slots"""
72 try:
73 signal.disconnect()
74 except TypeError: # allow unconnected slots
75 pass
78 def get(widget):
79 """Query a widget for its python value"""
80 if hasattr(widget, 'isChecked'):
81 value = widget.isChecked()
82 elif hasattr(widget, 'value'):
83 value = widget.value()
84 elif hasattr(widget, 'text'):
85 value = widget.text()
86 elif hasattr(widget, 'toPlainText'):
87 value = widget.toPlainText()
88 elif hasattr(widget, 'sizes'):
89 value = widget.sizes()
90 elif hasattr(widget, 'date'):
91 value = widget.date().toString(Qt.ISODate)
92 else:
93 value = None
94 return value
97 def hbox(margin, spacing, *items):
98 """Create an HBoxLayout with the specified sizes and items"""
99 return box(QtWidgets.QHBoxLayout, margin, spacing, *items)
102 def vbox(margin, spacing, *items):
103 """Create a VBoxLayout with the specified sizes and items"""
104 return box(QtWidgets.QVBoxLayout, margin, spacing, *items)
107 def buttongroup(*items):
108 """Create a QButtonGroup for the specified items"""
109 group = QtWidgets.QButtonGroup()
110 for i in items:
111 group.addButton(i)
112 return group
115 def set_margin(layout, margin):
116 """Set the content margins for a layout"""
117 layout.setContentsMargins(margin, margin, margin, margin)
120 def box(cls, margin, spacing, *items):
121 """Create a QBoxLayout with the specified sizes and items"""
122 stretch = STRETCH
123 skipped = SKIPPED
124 layout = cls()
125 layout.setSpacing(spacing)
126 set_margin(layout, margin)
128 for i in items:
129 if isinstance(i, QtWidgets.QWidget):
130 layout.addWidget(i)
131 elif isinstance(i, (QtWidgets.QHBoxLayout, QtWidgets.QVBoxLayout,
132 QtWidgets.QFormLayout, QtWidgets.QLayout)):
133 layout.addLayout(i)
134 elif i is stretch:
135 layout.addStretch()
136 elif i is skipped:
137 continue
138 elif isinstance(i, int_types):
139 layout.addSpacing(i)
141 return layout
144 def form(margin, spacing, *widgets):
145 """Create a QFormLayout with the specified sizes and items"""
146 layout = QtWidgets.QFormLayout()
147 layout.setSpacing(spacing)
148 layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
149 set_margin(layout, margin)
151 for idx, (name, widget) in enumerate(widgets):
152 if isinstance(name, (str, ustr)):
153 layout.addRow(name, widget)
154 else:
155 layout.setWidget(idx, QtWidgets.QFormLayout.LabelRole, name)
156 layout.setWidget(idx, QtWidgets.QFormLayout.FieldRole, widget)
158 return layout
161 def grid(margin, spacing, *widgets):
162 """Create a QGridLayout with the specified sizes and items"""
163 layout = QtWidgets.QGridLayout()
164 layout.setSpacing(spacing)
165 set_margin(layout, margin)
167 for row in widgets:
168 item = row[0]
169 if isinstance(item, QtWidgets.QWidget):
170 layout.addWidget(*row)
171 elif isinstance(item, QtWidgets.QLayoutItem):
172 layout.addItem(*row)
174 return layout
177 def splitter(orientation, *widgets):
178 """Create a spliter over the specified widgets
180 :param orientation: Qt.Horizontal or Qt.Vertical
183 layout = QtWidgets.QSplitter()
184 layout.setOrientation(orientation)
185 layout.setHandleWidth(defs.handle_width)
186 layout.setChildrenCollapsible(True)
188 for idx, widget in enumerate(widgets):
189 layout.addWidget(widget)
190 layout.setStretchFactor(idx, 1)
192 # Workaround for Qt not setting the WA_Hover property for QSplitter
193 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
194 layout.handle(1).setAttribute(Qt.WA_Hover)
196 return layout
199 def label(text=None, align=None, fmt=None, selectable=True):
200 """Create a QLabel with the specified properties"""
201 widget = QtWidgets.QLabel()
202 if align is not None:
203 widget.setAlignment(align)
204 if fmt is not None:
205 widget.setTextFormat(fmt)
206 if selectable:
207 widget.setTextInteractionFlags(Qt.TextBrowserInteraction)
208 widget.setOpenExternalLinks(True)
209 if text:
210 widget.setText(text)
211 return widget
214 class ComboBox(QtWidgets.QComboBox):
215 """Custom read-only combobox with a convenient API"""
217 def __init__(self, items=None, editable=False, parent=None):
218 super(ComboBox, self).__init__(parent)
219 self.setEditable(editable)
220 if items:
221 self.addItems(items)
223 def set_index(self, idx):
224 idx = utils.clamp(idx, 0, self.count()-1)
225 self.setCurrentIndex(idx)
228 def combo(items, editable=False, parent=None):
229 """Create a readonly (by default) combobox from a list of items"""
230 return ComboBox(editable=editable, items=items, parent=parent)
233 def textbrowser(text=None):
234 """Create a QTextBrowser for the specified text"""
235 widget = QtWidgets.QTextBrowser()
236 widget.setOpenExternalLinks(True)
237 if text:
238 widget.setText(text)
239 return widget
242 def add_completer(widget, items):
243 """Add simple completion to a widget"""
244 completer = QtWidgets.QCompleter(items, widget)
245 completer.setCaseSensitivity(Qt.CaseInsensitive)
246 completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion)
247 widget.setCompleter(completer)
250 def prompt(msg, title=None, text='', parent=None):
251 """Presents the user with an input widget and returns the input."""
252 if title is None:
253 title = msg
254 if parent is None:
255 parent = active_window()
256 result = QtWidgets.QInputDialog.getText(
257 parent, title, msg, QtWidgets.QLineEdit.Normal, text)
258 return (result[0], result[1])
261 def prompt_n(msg, inputs):
262 """Presents the user with N input widgets and returns the results"""
263 dialog = QtWidgets.QDialog(active_window())
264 dialog.setWindowModality(Qt.WindowModal)
265 dialog.setWindowTitle(msg)
267 long_value = msg
268 for k, v in inputs:
269 if len(k + v) > len(long_value):
270 long_value = k + v
272 metrics = QtGui.QFontMetrics(dialog.font())
273 min_width = metrics.width(long_value) + 100
274 if min_width > 720:
275 min_width = 720
276 dialog.setMinimumWidth(min_width)
278 ok_b = ok_button(msg, enabled=False)
279 close_b = close_button()
281 form_widgets = []
283 def get_values():
284 return [pair[1].text().strip() for pair in form_widgets]
286 for name, value in inputs:
287 lineedit = QtWidgets.QLineEdit()
288 # Enable the OK button only when all fields have been populated
289 lineedit.textChanged.connect(
290 lambda x: ok_b.setEnabled(all(get_values())))
291 if value:
292 lineedit.setText(value)
293 form_widgets.append((name, lineedit))
295 # layouts
296 form_layout = form(defs.no_margin, defs.button_spacing, *form_widgets)
297 button_layout = hbox(defs.no_margin, defs.button_spacing,
298 STRETCH, close_b, ok_b)
299 main_layout = vbox(defs.margin, defs.button_spacing,
300 form_layout, button_layout)
301 dialog.setLayout(main_layout)
303 # connections
304 connect_button(ok_b, dialog.accept)
305 connect_button(close_b, dialog.reject)
307 accepted = dialog.exec_() == QtWidgets.QDialog.Accepted
308 text = get_values()
309 ok = accepted and all(text)
310 return (ok, text)
313 class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
315 TYPE = QtGui.QStandardItem.UserType + 101
317 def __init__(self, path, icon, deleted):
318 QtWidgets.QTreeWidgetItem.__init__(self)
319 self.path = path
320 self.deleted = deleted
321 self.setIcon(0, icons.from_name(icon))
322 self.setText(0, path)
324 def type(self):
325 return self.TYPE
328 def paths_from_indexes(model, indexes,
329 item_type=TreeWidgetItem.TYPE,
330 item_filter=None):
331 """Return paths from a list of QStandardItemModel indexes"""
332 items = [model.itemFromIndex(i) for i in indexes]
333 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
336 def _true_filter(_x):
337 return True
340 def paths_from_items(items,
341 item_type=TreeWidgetItem.TYPE,
342 item_filter=None):
343 """Return a list of paths from a list of items"""
344 if item_filter is None:
345 item_filter = _true_filter
346 return [i.path for i in items
347 if i.type() == item_type and item_filter(i)]
350 def tree_selection(tree_item, items):
351 """Returns an array of model items that correspond to the selected
352 QTreeWidgetItem children"""
353 selected = []
354 count = min(tree_item.childCount(), len(items))
355 for idx in range(count):
356 if tree_item.child(idx).isSelected():
357 selected.append(items[idx])
359 return selected
362 def tree_selection_items(tree_item):
363 """Returns selected widget items"""
364 selected = []
365 for idx in range(tree_item.childCount()):
366 child = tree_item.child(idx)
367 if child.isSelected():
368 selected.append(child)
370 return selected
373 def selected_item(list_widget, items):
374 """Returns the model item that corresponds to the selected QListWidget
375 row."""
376 widget_items = list_widget.selectedItems()
377 if not widget_items:
378 return None
379 widget_item = widget_items[0]
380 row = list_widget.row(widget_item)
381 if row < len(items):
382 item = items[row]
383 else:
384 item = None
385 return item
388 def selected_items(list_widget, items):
389 """Returns an array of model items that correspond to the selected
390 QListWidget rows."""
391 item_count = len(items)
392 selected = []
393 for widget_item in list_widget.selectedItems():
394 row = list_widget.row(widget_item)
395 if row < item_count:
396 selected.append(items[row])
397 return selected
400 def open_file(title, directory=None):
401 """Creates an Open File dialog and returns a filename."""
402 result = compat.getopenfilename(parent=active_window(),
403 caption=title,
404 basedir=directory)
405 return result[0]
408 def open_files(title, directory=None, filters=''):
409 """Creates an Open File dialog and returns a list of filenames."""
410 result = compat.getopenfilenames(parent=active_window(),
411 caption=title,
412 basedir=directory,
413 filters=filters)
414 return result[0]
417 def opendir_dialog(caption, path):
418 """Prompts for a directory path"""
420 options = (QtWidgets.QFileDialog.ShowDirsOnly |
421 QtWidgets.QFileDialog.DontResolveSymlinks)
422 return compat.getexistingdirectory(parent=active_window(),
423 caption=caption,
424 basedir=path,
425 options=options)
428 def save_as(filename, title='Save As...'):
429 """Creates a Save File dialog and returns a filename."""
430 result = compat.getsavefilename(parent=active_window(),
431 caption=title,
432 basedir=filename)
433 return result[0]
436 def copy_path(filename, absolute=True):
437 """Copy a filename to the clipboard"""
438 if filename is None:
439 return
440 if absolute:
441 filename = core.abspath(filename)
442 set_clipboard(filename)
445 def set_clipboard(text):
446 """Sets the copy/paste buffer to text."""
447 if not text:
448 return
449 clipboard = QtWidgets.QApplication.clipboard()
450 clipboard.setText(text, QtGui.QClipboard.Clipboard)
451 clipboard.setText(text, QtGui.QClipboard.Selection)
452 persist_clipboard()
455 # pylint: disable=line-too-long
456 def persist_clipboard():
457 """Persist the clipboard
459 X11 stores only a reference to the clipboard data.
460 Send a clipboard event to force a copy of the clipboard to occur.
461 This ensures that the clipboard is present after git-cola exits.
462 Otherwise, the reference is destroyed on exit.
464 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
466 """ # noqa
467 clipboard = QtWidgets.QApplication.clipboard()
468 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
469 QtWidgets.QApplication.sendEvent(clipboard, event)
472 def add_action_bool(widget, text, fn, checked, *shortcuts):
473 tip = text
474 action = _add_action(widget, text, tip, fn,
475 connect_action_bool, *shortcuts)
476 action.setCheckable(True)
477 action.setChecked(checked)
478 return action
481 def add_action(widget, text, fn, *shortcuts):
482 tip = text
483 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
486 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
487 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
490 def _add_action(widget, text, tip, fn, connect, *shortcuts):
491 action = QtWidgets.QAction(text, widget)
492 if hasattr(action, 'setIconVisibleInMenu'):
493 action.setIconVisibleInMenu(True)
494 if tip:
495 action.setStatusTip(tip)
496 connect(action, fn)
497 if shortcuts:
498 action.setShortcuts(shortcuts)
499 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
500 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
501 widget.addAction(action)
502 return action
505 def set_selected_item(widget, idx):
506 """Sets a the currently selected item to the item at index idx."""
507 if isinstance(widget, QtWidgets.QTreeWidget):
508 item = widget.topLevelItem(idx)
509 if item:
510 item.setSelected(True)
511 widget.setCurrentItem(item)
514 def add_items(widget, items):
515 """Adds items to a widget."""
516 for item in items:
517 if item is None:
518 continue
519 widget.addItem(item)
522 def set_items(widget, items):
523 """Clear the existing widget contents and set the new items."""
524 widget.clear()
525 add_items(widget, items)
528 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
529 """Given a filename, return a TreeWidgetItem for a status widget
531 "staged", "deleted, and "untracked" control which icon is used.
534 icon_name = icons.status(filename, deleted, staged, untracked)
535 return TreeWidgetItem(filename, icons.name_from_basename(icon_name),
536 deleted=deleted)
539 def add_close_action(widget):
540 """Adds close action and shortcuts to a widget."""
541 return add_action(widget, N_('Close...'),
542 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
545 def app():
546 """Return the current application"""
547 return QtWidgets.QApplication.instance()
550 def desktop():
551 """Return the desktop"""
552 return app().desktop()
555 def desktop_size():
556 desk = desktop()
557 rect = desk.screenGeometry(QtGui.QCursor().pos())
558 return (rect.width(), rect.height())
561 def center_on_screen(widget):
562 """Move widget to the center of the default screen"""
563 width, height = desktop_size()
564 cx = width // 2
565 cy = height // 2
566 widget.move(cx - widget.width()//2, cy - widget.height()//2)
569 def default_size(parent, width, height, use_parent_height=True):
570 """Return the parent's size, or the provided defaults"""
571 if parent is not None:
572 width = parent.width()
573 if use_parent_height:
574 height = parent.height()
575 return (width, height)
578 def default_monospace_font():
579 if utils.is_darwin():
580 family = 'Monaco'
581 else:
582 family = 'Monospace'
583 mfont = QtGui.QFont()
584 mfont.setFamily(family)
585 return mfont
588 def diff_font_str(context):
589 cfg = context.cfg
590 font_str = cfg.get(prefs.FONTDIFF)
591 if not font_str:
592 font_str = default_monospace_font().toString()
593 return font_str
596 def diff_font(context):
597 return font(diff_font_str(context))
600 def font(string):
601 qfont = QtGui.QFont()
602 qfont.fromString(string)
603 return qfont
606 def create_button(text='', layout=None, tooltip=None, icon=None,
607 enabled=True, default=False):
608 """Create a button, set its title, and add it to the parent."""
609 button = QtWidgets.QPushButton()
610 button.setCursor(Qt.PointingHandCursor)
611 button.setFocusPolicy(Qt.NoFocus)
612 if text:
613 button.setText(' ' + text)
614 if icon is not None:
615 button.setIcon(icon)
616 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
617 if tooltip is not None:
618 button.setToolTip(tooltip)
619 if layout is not None:
620 layout.addWidget(button)
621 if not enabled:
622 button.setEnabled(False)
623 if default:
624 button.setDefault(True)
625 return button
628 def create_action_button(tooltip=None, icon=None):
629 button = QtWidgets.QPushButton()
630 button.setCursor(Qt.PointingHandCursor)
631 button.setFocusPolicy(Qt.NoFocus)
632 button.setFlat(True)
633 if tooltip is not None:
634 button.setToolTip(tooltip)
635 if icon is not None:
636 button.setIcon(icon)
637 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
638 return button
641 def ok_button(text, default=True, enabled=True, icon=None):
642 if icon is None:
643 icon = icons.ok()
644 return create_button(text=text, icon=icon, default=default, enabled=enabled)
647 def close_button(text=None, icon=None):
648 text = text or N_('Close')
649 icon = icons.mkicon(icon, icons.close)
650 return create_button(text=text, icon=icon)
653 def edit_button(enabled=True, default=False):
654 return create_button(text=N_('Edit'), icon=icons.edit(),
655 enabled=enabled, default=default)
658 def refresh_button(enabled=True, default=False):
659 return create_button(text=N_('Refresh'), icon=icons.sync(),
660 enabled=enabled, default=default)
663 def hide_button_menu_indicator(button):
664 """Hide the menu indicator icon on buttons"""
666 name = button.__class__.__name__
667 stylesheet = """
668 %(name)s::menu-indicator {
669 image: none;
672 if name == 'QPushButton':
673 stylesheet += """
674 %(name)s {
675 border-style: none;
678 button.setStyleSheet(stylesheet % dict(name=name))
681 def checkbox(text='', tooltip='', checked=None):
682 """Create a checkbox"""
683 return _checkbox(QtWidgets.QCheckBox, text, tooltip, checked)
686 def radio(text='', tooltip='', checked=None):
687 """Create a radio button"""
688 return _checkbox(QtWidgets.QRadioButton, text, tooltip, checked)
691 def _checkbox(cls, text, tooltip, checked):
692 """Create a widget and apply properties"""
693 widget = cls()
694 if text:
695 widget.setText(text)
696 if tooltip:
697 widget.setToolTip(tooltip)
698 if checked is not None:
699 widget.setChecked(checked)
700 return widget
703 class DockTitleBarWidget(QtWidgets.QWidget):
705 def __init__(self, parent, title, stretch=True):
706 QtWidgets.QWidget.__init__(self, parent)
707 self.setAutoFillBackground(True)
708 self.label = qlabel = QtWidgets.QLabel(title, self)
709 qfont = qlabel.font()
710 qfont.setBold(True)
711 qlabel.setFont(qfont)
712 qlabel.setCursor(Qt.OpenHandCursor)
714 self.close_button = create_action_button(
715 tooltip=N_('Close'), icon=icons.close())
717 self.toggle_button = create_action_button(
718 tooltip=N_('Detach'), icon=icons.external())
720 self.corner_layout = hbox(defs.no_margin, defs.spacing)
722 if stretch:
723 separator = STRETCH
724 else:
725 separator = SKIPPED
727 self.main_layout = hbox(defs.small_margin, defs.spacing,
728 qlabel, separator, self.corner_layout,
729 self.toggle_button, self.close_button)
730 self.setLayout(self.main_layout)
732 connect_button(self.toggle_button, self.toggle_floating)
733 connect_button(self.close_button, self.toggle_visibility)
735 def toggle_floating(self):
736 self.parent().setFloating(not self.parent().isFloating())
737 self.update_tooltips()
739 def toggle_visibility(self):
740 self.parent().toggleViewAction().trigger()
742 def set_title(self, title):
743 self.label.setText(title)
745 def add_corner_widget(self, widget):
746 self.corner_layout.addWidget(widget)
748 def update_tooltips(self):
749 if self.parent().isFloating():
750 tooltip = N_('Attach')
751 else:
752 tooltip = N_('Detach')
753 self.toggle_button.setToolTip(tooltip)
756 def create_dock(title, parent, stretch=True, widget=None, fn=None):
757 """Create a dock widget and set it up accordingly."""
758 dock = QtWidgets.QDockWidget(parent)
759 dock.setWindowTitle(title)
760 dock.setObjectName(title)
761 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
762 dock.setTitleBarWidget(titlebar)
763 dock.setAutoFillBackground(True)
764 if hasattr(parent, 'dockwidgets'):
765 parent.dockwidgets.append(dock)
766 if fn:
767 widget = fn(dock)
768 if widget:
769 dock.setWidget(widget)
770 return dock
773 def hide_dock(widget):
774 widget.toggleViewAction().setChecked(False)
775 widget.hide()
778 def create_menu(title, parent):
779 """Create a menu and set its title."""
780 qmenu = DebouncingMenu(title, parent)
781 return qmenu
784 class DebouncingMenu(QtWidgets.QMenu):
785 """Menu that debounces mouse release action ie. stops it if occurred
786 right after menu creation.
788 Disables annoying behaviour when RMB is pressed to show menu, cursor is
789 moved accidentally 1px onto newly created menu and released causing to
790 execute menu action
793 threshold_ms = 400
795 def __init__(self, title, parent):
796 QtWidgets.QMenu.__init__(self, title, parent)
797 self.created_at = utils.epoch_millis()
799 def mouseReleaseEvent(self, event):
800 threshold = DebouncingMenu.threshold_ms
801 if (utils.epoch_millis() - self.created_at) > threshold:
802 QtWidgets.QMenu.mouseReleaseEvent(self, event)
805 def add_menu(title, parent):
806 """Create a menu and set its title."""
807 menu = create_menu(title, parent)
808 parent.addAction(menu.menuAction())
809 return menu
812 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
813 button = QtWidgets.QToolButton()
814 button.setAutoRaise(True)
815 button.setAutoFillBackground(True)
816 button.setCursor(Qt.PointingHandCursor)
817 button.setFocusPolicy(Qt.NoFocus)
818 if icon is not None:
819 button.setIcon(icon)
820 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
821 if text is not None:
822 button.setText(' ' + text)
823 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
824 if tooltip is not None:
825 button.setToolTip(tooltip)
826 if layout is not None:
827 layout.addWidget(button)
828 return button
831 # pylint: disable=line-too-long
832 def mimedata_from_paths(context, paths):
833 """Return mimedata with a list of absolute path URLs
835 The text/x-moz-list format is always included by Qt, and doing
836 mimedata.removeFormat('text/x-moz-url') has no effect.
837 C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
839 gnome-terminal expects utf-16 encoded text, but other terminals,
840 e.g. terminator, prefer utf-8, so allow cola.dragencoding
841 to override the default.
843 """ # noqa
844 cfg = context.cfg
845 abspaths = [core.abspath(path) for path in paths]
846 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
848 mimedata = QtCore.QMimeData()
849 mimedata.setUrls(urls)
851 paths_text = core.list2cmdline(abspaths)
852 encoding = cfg.get('cola.dragencoding', 'utf-16')
853 moz_text = core.encode(paths_text, encoding=encoding)
854 mimedata.setData('text/x-moz-url', moz_text)
856 return mimedata
859 def path_mimetypes():
860 return ['text/uri-list', 'text/x-moz-url']
863 class BlockSignals(object):
864 """Context manager for blocking a signals on a widget"""
866 def __init__(self, *widgets):
867 self.widgets = widgets
868 self.values = {}
870 def __enter__(self):
871 for w in self.widgets:
872 self.values[w] = w.blockSignals(True)
873 return self
875 def __exit__(self, exc_type, exc_val, exc_tb):
876 for w in self.widgets:
877 w.blockSignals(self.values[w])
880 class Channel(QtCore.QObject):
881 finished = Signal(object)
882 result = Signal(object)
885 class Task(QtCore.QRunnable):
886 """Disable auto-deletion to avoid gc issues
888 Python's garbage collector will try to double-free the task
889 once it's finished, so disable Qt's auto-deletion as a workaround.
893 def __init__(self, parent):
894 QtCore.QRunnable.__init__(self)
896 self.channel = Channel(parent)
897 self.result = None
898 self.setAutoDelete(False)
900 def run(self):
901 self.result = self.task()
902 self.channel.result.emit(self.result)
903 self.channel.finished.emit(self)
905 # pylint: disable=no-self-use
906 def task(self):
907 return None
909 def connect(self, handler):
910 self.channel.result.connect(handler, type=Qt.QueuedConnection)
913 class SimpleTask(Task):
914 """Run a simple callable as a task"""
916 def __init__(self, parent, fn, *args, **kwargs):
917 Task.__init__(self, parent)
919 self.fn = fn
920 self.args = args
921 self.kwargs = kwargs
923 def task(self):
924 return self.fn(*self.args, **self.kwargs)
927 class RunTask(QtCore.QObject):
928 """Runs QRunnable instances and transfers control when they finish"""
930 def __init__(self, parent=None):
931 QtCore.QObject.__init__(self, parent)
932 self.tasks = []
933 self.task_details = {}
934 self.threadpool = QtCore.QThreadPool.globalInstance()
935 self.result_fn = None
937 def start(self, task, progress=None, finish=None, result=None):
938 """Start the task and register a callback"""
939 self.result_fn = result
940 if progress is not None:
941 progress.show()
942 # prevents garbage collection bugs in certain PyQt4 versions
943 self.tasks.append(task)
944 task_id = id(task)
945 self.task_details[task_id] = (progress, finish, result)
946 task.channel.finished.connect(self.finish, type=Qt.QueuedConnection)
947 self.threadpool.start(task)
949 def finish(self, task):
950 task_id = id(task)
951 try:
952 self.tasks.remove(task)
953 except ValueError:
954 pass
955 try:
956 progress, finish, result = self.task_details[task_id]
957 del self.task_details[task_id]
958 except KeyError:
959 finish = progress = result = None
961 if progress is not None:
962 progress.hide()
964 if result is not None:
965 result(task.result)
967 if finish is not None:
968 finish(task)
971 # Syntax highlighting
973 def rgb(r, g, b):
974 color = QtGui.QColor()
975 color.setRgb(r, g, b)
976 return color
979 def rgba(r, g, b, a=255):
980 color = rgb(r, g, b)
981 color.setAlpha(a)
982 return color
985 def RGB(args):
986 return rgb(*args)
989 def rgb_css(color):
990 """Convert a QColor into an rgb(int, int, int) CSS string"""
991 return 'rgb(%d, %d, %d)' % (color.red(), color.green(), color.blue())
994 def rgb_hex(color):
995 """Convert a QColor into a hex aabbcc string"""
996 return '%02x%02x%02x' % (color.red(), color.green(), color.blue())
999 def make_format(fg=None, bg=None, bold=False):
1000 fmt = QtGui.QTextCharFormat()
1001 if fg:
1002 fmt.setForeground(fg)
1003 if bg:
1004 fmt.setBackground(bg)
1005 if bold:
1006 fmt.setFontWeight(QtGui.QFont.Bold)
1007 return fmt
1010 class ImageFormats(object):
1012 def __init__(self):
1013 # returns a list of QByteArray objects
1014 formats_qba = QtGui.QImageReader.supportedImageFormats()
1015 # portability: python3 data() returns bytes, python2 returns str
1016 decode = core.decode
1017 formats = [decode(x.data()) for x in formats_qba]
1018 self.extensions = set(['.' + fmt for fmt in formats])
1020 def ok(self, filename):
1021 _, ext = os.path.splitext(filename)
1022 return ext.lower() in self.extensions