doc/relnotes: add #1000 and #1005 to the v3.6 release notes draft
[git-cola.git] / cola / qtutils.py
blob581cf12eb66190f0db2689a6077361367d201302
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, transform=None):
218 super(ComboBox, self).__init__(parent)
219 self.setEditable(editable)
220 self.transform = transform
221 self.item_data = []
222 if items:
223 self.addItems(items)
224 self.item_data.extend(items)
226 def set_index(self, idx):
227 idx = utils.clamp(idx, 0, self.count()-1)
228 self.setCurrentIndex(idx)
230 def add_item(self, text, data):
231 self.addItem(text)
232 self.item_data.append(data)
234 def current_data(self):
235 return self.item_data[self.currentIndex()]
237 def set_value(self, value):
238 if self.transform:
239 value = self.transform(value)
240 try:
241 index = self.item_data.index(value)
242 except ValueError:
243 index = 0
244 self.setCurrentIndex(index)
247 def combo(items, editable=False, parent=None):
248 """Create a readonly (by default) combobox from a list of items"""
249 return ComboBox(editable=editable, items=items, parent=parent)
252 def combo_mapped(data, editable=False, transform=None, parent=None):
253 """Create a readonly (by default) combobox from a list of items"""
254 widget = ComboBox(editable=editable, transform=transform, parent=parent)
255 for (k, v) in data:
256 widget.add_item(k, v)
257 return widget
260 def textbrowser(text=None):
261 """Create a QTextBrowser for the specified text"""
262 widget = QtWidgets.QTextBrowser()
263 widget.setOpenExternalLinks(True)
264 if text:
265 widget.setText(text)
266 return widget
269 def add_completer(widget, items):
270 """Add simple completion to a widget"""
271 completer = QtWidgets.QCompleter(items, widget)
272 completer.setCaseSensitivity(Qt.CaseInsensitive)
273 completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion)
274 widget.setCompleter(completer)
277 def prompt(msg, title=None, text='', parent=None):
278 """Presents the user with an input widget and returns the input."""
279 if title is None:
280 title = msg
281 if parent is None:
282 parent = active_window()
283 result = QtWidgets.QInputDialog.getText(
284 parent, title, msg, QtWidgets.QLineEdit.Normal, text)
285 return (result[0], result[1])
288 def prompt_n(msg, inputs):
289 """Presents the user with N input widgets and returns the results"""
290 dialog = QtWidgets.QDialog(active_window())
291 dialog.setWindowModality(Qt.WindowModal)
292 dialog.setWindowTitle(msg)
294 long_value = msg
295 for k, v in inputs:
296 if len(k + v) > len(long_value):
297 long_value = k + v
299 metrics = QtGui.QFontMetrics(dialog.font())
300 min_width = metrics.width(long_value) + 100
301 if min_width > 720:
302 min_width = 720
303 dialog.setMinimumWidth(min_width)
305 ok_b = ok_button(msg, enabled=False)
306 close_b = close_button()
308 form_widgets = []
310 def get_values():
311 return [pair[1].text().strip() for pair in form_widgets]
313 for name, value in inputs:
314 lineedit = QtWidgets.QLineEdit()
315 # Enable the OK button only when all fields have been populated
316 # pylint: disable=no-member
317 lineedit.textChanged.connect(
318 lambda x: ok_b.setEnabled(all(get_values())))
319 if value:
320 lineedit.setText(value)
321 form_widgets.append((name, lineedit))
323 # layouts
324 form_layout = form(defs.no_margin, defs.button_spacing, *form_widgets)
325 button_layout = hbox(defs.no_margin, defs.button_spacing,
326 STRETCH, close_b, ok_b)
327 main_layout = vbox(defs.margin, defs.button_spacing,
328 form_layout, button_layout)
329 dialog.setLayout(main_layout)
331 # connections
332 connect_button(ok_b, dialog.accept)
333 connect_button(close_b, dialog.reject)
335 accepted = dialog.exec_() == QtWidgets.QDialog.Accepted
336 text = get_values()
337 ok = accepted and all(text)
338 return (ok, text)
341 class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
343 TYPE = QtGui.QStandardItem.UserType + 101
345 def __init__(self, path, icon, deleted):
346 QtWidgets.QTreeWidgetItem.__init__(self)
347 self.path = path
348 self.deleted = deleted
349 self.setIcon(0, icons.from_name(icon))
350 self.setText(0, path)
352 def type(self):
353 return self.TYPE
356 def paths_from_indexes(model, indexes,
357 item_type=TreeWidgetItem.TYPE,
358 item_filter=None):
359 """Return paths from a list of QStandardItemModel indexes"""
360 items = [model.itemFromIndex(i) for i in indexes]
361 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
364 def _true_filter(_x):
365 return True
368 def paths_from_items(items,
369 item_type=TreeWidgetItem.TYPE,
370 item_filter=None):
371 """Return a list of paths from a list of items"""
372 if item_filter is None:
373 item_filter = _true_filter
374 return [i.path for i in items
375 if i.type() == item_type and item_filter(i)]
378 def tree_selection(tree_item, items):
379 """Returns an array of model items that correspond to the selected
380 QTreeWidgetItem children"""
381 selected = []
382 count = min(tree_item.childCount(), len(items))
383 for idx in range(count):
384 if tree_item.child(idx).isSelected():
385 selected.append(items[idx])
387 return selected
390 def tree_selection_items(tree_item):
391 """Returns selected widget items"""
392 selected = []
393 for idx in range(tree_item.childCount()):
394 child = tree_item.child(idx)
395 if child.isSelected():
396 selected.append(child)
398 return selected
401 def selected_item(list_widget, items):
402 """Returns the model item that corresponds to the selected QListWidget
403 row."""
404 widget_items = list_widget.selectedItems()
405 if not widget_items:
406 return None
407 widget_item = widget_items[0]
408 row = list_widget.row(widget_item)
409 if row < len(items):
410 item = items[row]
411 else:
412 item = None
413 return item
416 def selected_items(list_widget, items):
417 """Returns an array of model items that correspond to the selected
418 QListWidget rows."""
419 item_count = len(items)
420 selected = []
421 for widget_item in list_widget.selectedItems():
422 row = list_widget.row(widget_item)
423 if row < item_count:
424 selected.append(items[row])
425 return selected
428 def open_file(title, directory=None):
429 """Creates an Open File dialog and returns a filename."""
430 result = compat.getopenfilename(parent=active_window(),
431 caption=title,
432 basedir=directory)
433 return result[0]
436 def open_files(title, directory=None, filters=''):
437 """Creates an Open File dialog and returns a list of filenames."""
438 result = compat.getopenfilenames(parent=active_window(),
439 caption=title,
440 basedir=directory,
441 filters=filters)
442 return result[0]
445 def opendir_dialog(caption, path):
446 """Prompts for a directory path"""
448 options = (QtWidgets.QFileDialog.ShowDirsOnly |
449 QtWidgets.QFileDialog.DontResolveSymlinks)
450 return compat.getexistingdirectory(parent=active_window(),
451 caption=caption,
452 basedir=path,
453 options=options)
456 def save_as(filename, title='Save As...'):
457 """Creates a Save File dialog and returns a filename."""
458 result = compat.getsavefilename(parent=active_window(),
459 caption=title,
460 basedir=filename)
461 return result[0]
464 def copy_path(filename, absolute=True):
465 """Copy a filename to the clipboard"""
466 if filename is None:
467 return
468 if absolute:
469 filename = core.abspath(filename)
470 set_clipboard(filename)
473 def set_clipboard(text):
474 """Sets the copy/paste buffer to text."""
475 if not text:
476 return
477 clipboard = QtWidgets.QApplication.clipboard()
478 clipboard.setText(text, QtGui.QClipboard.Clipboard)
479 clipboard.setText(text, QtGui.QClipboard.Selection)
480 persist_clipboard()
483 # pylint: disable=line-too-long
484 def persist_clipboard():
485 """Persist the clipboard
487 X11 stores only a reference to the clipboard data.
488 Send a clipboard event to force a copy of the clipboard to occur.
489 This ensures that the clipboard is present after git-cola exits.
490 Otherwise, the reference is destroyed on exit.
492 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
494 """ # noqa
495 clipboard = QtWidgets.QApplication.clipboard()
496 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
497 QtWidgets.QApplication.sendEvent(clipboard, event)
500 def add_action_bool(widget, text, fn, checked, *shortcuts):
501 tip = text
502 action = _add_action(widget, text, tip, fn,
503 connect_action_bool, *shortcuts)
504 action.setCheckable(True)
505 action.setChecked(checked)
506 return action
509 def add_action(widget, text, fn, *shortcuts):
510 tip = text
511 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
514 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
515 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
518 def _add_action(widget, text, tip, fn, connect, *shortcuts):
519 action = QtWidgets.QAction(text, widget)
520 if hasattr(action, 'setIconVisibleInMenu'):
521 action.setIconVisibleInMenu(True)
522 if tip:
523 action.setStatusTip(tip)
524 connect(action, fn)
525 if shortcuts:
526 action.setShortcuts(shortcuts)
527 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
528 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
529 widget.addAction(action)
530 return action
533 def set_selected_item(widget, idx):
534 """Sets a the currently selected item to the item at index idx."""
535 if isinstance(widget, QtWidgets.QTreeWidget):
536 item = widget.topLevelItem(idx)
537 if item:
538 item.setSelected(True)
539 widget.setCurrentItem(item)
542 def add_items(widget, items):
543 """Adds items to a widget."""
544 for item in items:
545 if item is None:
546 continue
547 widget.addItem(item)
550 def set_items(widget, items):
551 """Clear the existing widget contents and set the new items."""
552 widget.clear()
553 add_items(widget, items)
556 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
557 """Given a filename, return a TreeWidgetItem for a status widget
559 "staged", "deleted, and "untracked" control which icon is used.
562 icon_name = icons.status(filename, deleted, staged, untracked)
563 return TreeWidgetItem(filename, icons.name_from_basename(icon_name),
564 deleted=deleted)
567 def add_close_action(widget):
568 """Adds close action and shortcuts to a widget."""
569 return add_action(widget, N_('Close...'),
570 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
573 def app():
574 """Return the current application"""
575 return QtWidgets.QApplication.instance()
578 def desktop():
579 """Return the desktop"""
580 return app().desktop()
583 def desktop_size():
584 desk = desktop()
585 rect = desk.screenGeometry(QtGui.QCursor().pos())
586 return (rect.width(), rect.height())
589 def center_on_screen(widget):
590 """Move widget to the center of the default screen"""
591 width, height = desktop_size()
592 cx = width // 2
593 cy = height // 2
594 widget.move(cx - widget.width()//2, cy - widget.height()//2)
597 def default_size(parent, width, height, use_parent_height=True):
598 """Return the parent's size, or the provided defaults"""
599 if parent is not None:
600 width = parent.width()
601 if use_parent_height:
602 height = parent.height()
603 return (width, height)
606 def default_monospace_font():
607 if utils.is_darwin():
608 family = 'Monaco'
609 else:
610 family = 'Monospace'
611 mfont = QtGui.QFont()
612 mfont.setFamily(family)
613 return mfont
616 def diff_font_str(context):
617 cfg = context.cfg
618 font_str = cfg.get(prefs.FONTDIFF)
619 if not font_str:
620 font_str = default_monospace_font().toString()
621 return font_str
624 def diff_font(context):
625 return font(diff_font_str(context))
628 def font(string):
629 qfont = QtGui.QFont()
630 qfont.fromString(string)
631 return qfont
634 def create_button(text='', layout=None, tooltip=None, icon=None,
635 enabled=True, default=False):
636 """Create a button, set its title, and add it to the parent."""
637 button = QtWidgets.QPushButton()
638 button.setCursor(Qt.PointingHandCursor)
639 button.setFocusPolicy(Qt.NoFocus)
640 if text:
641 button.setText(' ' + text)
642 if icon is not None:
643 button.setIcon(icon)
644 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
645 if tooltip is not None:
646 button.setToolTip(tooltip)
647 if layout is not None:
648 layout.addWidget(button)
649 if not enabled:
650 button.setEnabled(False)
651 if default:
652 button.setDefault(True)
653 return button
656 def tool_button():
657 """Create a flat border-less button"""
658 button = QtWidgets.QToolButton()
659 button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
660 button.setCursor(Qt.PointingHandCursor)
661 button.setFocusPolicy(Qt.NoFocus)
662 # Highlight colors
663 palette = QtGui.QPalette()
664 highlight = palette.color(QtGui.QPalette.Highlight)
665 highlight_rgb = rgb_css(highlight)
667 button.setStyleSheet("""
668 /* No borders */
669 QToolButton {
670 border: none;
671 background-color: none;
673 /* Hide the menu indicator */
674 QToolButton::menu-indicator {
675 image: none;
677 QToolButton:hover {
678 border: %(border)spx solid %(highlight_rgb)s;
680 """ % dict(border=defs.border, highlight_rgb=highlight_rgb))
681 return button
684 def create_action_button(tooltip=None, icon=None):
685 """Create a small toolbutton for use in dock title widgets"""
686 button = tool_button()
687 if tooltip is not None:
688 button.setToolTip(tooltip)
689 if icon is not None:
690 button.setIcon(icon)
691 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
692 return button
695 def ok_button(text, default=True, enabled=True, icon=None):
696 if icon is None:
697 icon = icons.ok()
698 return create_button(text=text, icon=icon, default=default, enabled=enabled)
701 def close_button(text=None, icon=None):
702 text = text or N_('Close')
703 icon = icons.mkicon(icon, icons.close)
704 return create_button(text=text, icon=icon)
707 def edit_button(enabled=True, default=False):
708 return create_button(text=N_('Edit'), icon=icons.edit(),
709 enabled=enabled, default=default)
712 def refresh_button(enabled=True, default=False):
713 return create_button(text=N_('Refresh'), icon=icons.sync(),
714 enabled=enabled, default=default)
717 def checkbox(text='', tooltip='', checked=None):
718 """Create a checkbox"""
719 return _checkbox(QtWidgets.QCheckBox, text, tooltip, checked)
722 def radio(text='', tooltip='', checked=None):
723 """Create a radio button"""
724 return _checkbox(QtWidgets.QRadioButton, text, tooltip, checked)
727 def _checkbox(cls, text, tooltip, checked):
728 """Create a widget and apply properties"""
729 widget = cls()
730 if text:
731 widget.setText(text)
732 if tooltip:
733 widget.setToolTip(tooltip)
734 if checked is not None:
735 widget.setChecked(checked)
736 return widget
739 class DockTitleBarWidget(QtWidgets.QFrame):
741 def __init__(self, parent, title, stretch=True):
742 QtWidgets.QFrame.__init__(self, parent)
743 self.setAutoFillBackground(True)
744 self.label = qlabel = QtWidgets.QLabel(title, self)
745 qfont = qlabel.font()
746 qfont.setBold(True)
747 qlabel.setFont(qfont)
748 qlabel.setCursor(Qt.OpenHandCursor)
750 self.close_button = create_action_button(
751 tooltip=N_('Close'), icon=icons.close())
753 self.toggle_button = create_action_button(
754 tooltip=N_('Detach'), icon=icons.external())
756 self.corner_layout = hbox(defs.no_margin, defs.spacing)
758 if stretch:
759 separator = STRETCH
760 else:
761 separator = SKIPPED
763 self.main_layout = hbox(defs.small_margin, defs.titlebar_spacing,
764 qlabel, separator, self.corner_layout,
765 self.toggle_button, self.close_button)
766 self.setLayout(self.main_layout)
768 connect_button(self.toggle_button, self.toggle_floating)
769 connect_button(self.close_button, self.toggle_visibility)
771 def toggle_floating(self):
772 self.parent().setFloating(not self.parent().isFloating())
773 self.update_tooltips()
775 def toggle_visibility(self):
776 self.parent().toggleViewAction().trigger()
778 def set_title(self, title):
779 self.label.setText(title)
781 def add_corner_widget(self, widget):
782 self.corner_layout.addWidget(widget)
784 def update_tooltips(self):
785 if self.parent().isFloating():
786 tooltip = N_('Attach')
787 else:
788 tooltip = N_('Detach')
789 self.toggle_button.setToolTip(tooltip)
792 def create_dock(title, parent, stretch=True, widget=None, fn=None):
793 """Create a dock widget and set it up accordingly."""
794 dock = QtWidgets.QDockWidget(parent)
795 dock.setWindowTitle(title)
796 dock.setObjectName(title)
797 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
798 dock.setTitleBarWidget(titlebar)
799 dock.setAutoFillBackground(True)
800 if hasattr(parent, 'dockwidgets'):
801 parent.dockwidgets.append(dock)
802 if fn:
803 widget = fn(dock)
804 assert isinstance(widget, QtWidgets.QFrame),\
805 "Docked widget has to be a QFrame"
806 if widget:
807 dock.setWidget(widget)
808 return dock
811 def hide_dock(widget):
812 widget.toggleViewAction().setChecked(False)
813 widget.hide()
816 def create_menu(title, parent):
817 """Create a menu and set its title."""
818 qmenu = DebouncingMenu(title, parent)
819 return qmenu
822 class DebouncingMenu(QtWidgets.QMenu):
823 """Menu that debounces mouse release action ie. stops it if occurred
824 right after menu creation.
826 Disables annoying behaviour when RMB is pressed to show menu, cursor is
827 moved accidentally 1px onto newly created menu and released causing to
828 execute menu action
831 threshold_ms = 400
833 def __init__(self, title, parent):
834 QtWidgets.QMenu.__init__(self, title, parent)
835 self.created_at = utils.epoch_millis()
837 def mouseReleaseEvent(self, event):
838 threshold = DebouncingMenu.threshold_ms
839 if (utils.epoch_millis() - self.created_at) > threshold:
840 QtWidgets.QMenu.mouseReleaseEvent(self, event)
843 def add_menu(title, parent):
844 """Create a menu and set its title."""
845 menu = create_menu(title, parent)
846 parent.addAction(menu.menuAction())
847 return menu
850 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
851 button = tool_button()
852 if icon is not None:
853 button.setIcon(icon)
854 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
855 if text is not None:
856 button.setText(' ' + text)
857 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
858 if tooltip is not None:
859 button.setToolTip(tooltip)
860 if layout is not None:
861 layout.addWidget(button)
862 return button
865 # pylint: disable=line-too-long
866 def mimedata_from_paths(context, paths):
867 """Return mimedata with a list of absolute path URLs
869 The text/x-moz-list format is always included by Qt, and doing
870 mimedata.removeFormat('text/x-moz-url') has no effect.
871 C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
873 gnome-terminal expects utf-16 encoded text, but other terminals,
874 e.g. terminator, prefer utf-8, so allow cola.dragencoding
875 to override the default.
877 """ # noqa
878 cfg = context.cfg
879 abspaths = [core.abspath(path) for path in paths]
880 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
882 mimedata = QtCore.QMimeData()
883 mimedata.setUrls(urls)
885 paths_text = core.list2cmdline(abspaths)
886 encoding = cfg.get('cola.dragencoding', 'utf-16')
887 moz_text = core.encode(paths_text, encoding=encoding)
888 mimedata.setData('text/x-moz-url', moz_text)
890 return mimedata
893 def path_mimetypes():
894 return ['text/uri-list', 'text/x-moz-url']
897 class BlockSignals(object):
898 """Context manager for blocking a signals on a widget"""
900 def __init__(self, *widgets):
901 self.widgets = widgets
902 self.values = {}
904 def __enter__(self):
905 for w in self.widgets:
906 self.values[w] = w.blockSignals(True)
907 return self
909 def __exit__(self, exc_type, exc_val, exc_tb):
910 for w in self.widgets:
911 w.blockSignals(self.values[w])
914 class Channel(QtCore.QObject):
915 finished = Signal(object)
916 result = Signal(object)
919 class Task(QtCore.QRunnable):
920 """Disable auto-deletion to avoid gc issues
922 Python's garbage collector will try to double-free the task
923 once it's finished, so disable Qt's auto-deletion as a workaround.
927 def __init__(self, parent):
928 QtCore.QRunnable.__init__(self)
930 self.channel = Channel(parent)
931 self.result = None
932 self.setAutoDelete(False)
934 def run(self):
935 self.result = self.task()
936 self.channel.result.emit(self.result)
937 self.channel.finished.emit(self)
939 # pylint: disable=no-self-use
940 def task(self):
941 return None
943 def connect(self, handler):
944 self.channel.result.connect(handler, type=Qt.QueuedConnection)
947 class SimpleTask(Task):
948 """Run a simple callable as a task"""
950 def __init__(self, parent, fn, *args, **kwargs):
951 Task.__init__(self, parent)
953 self.fn = fn
954 self.args = args
955 self.kwargs = kwargs
957 def task(self):
958 return self.fn(*self.args, **self.kwargs)
961 class RunTask(QtCore.QObject):
962 """Runs QRunnable instances and transfers control when they finish"""
964 def __init__(self, parent=None):
965 QtCore.QObject.__init__(self, parent)
966 self.tasks = []
967 self.task_details = {}
968 self.threadpool = QtCore.QThreadPool.globalInstance()
969 self.result_fn = None
971 def start(self, task, progress=None, finish=None, result=None):
972 """Start the task and register a callback"""
973 self.result_fn = result
974 if progress is not None:
975 progress.show()
976 # prevents garbage collection bugs in certain PyQt4 versions
977 self.tasks.append(task)
978 task_id = id(task)
979 self.task_details[task_id] = (progress, finish, result)
980 task.channel.finished.connect(self.finish, type=Qt.QueuedConnection)
981 self.threadpool.start(task)
983 def finish(self, task):
984 task_id = id(task)
985 try:
986 self.tasks.remove(task)
987 except ValueError:
988 pass
989 try:
990 progress, finish, result = self.task_details[task_id]
991 del self.task_details[task_id]
992 except KeyError:
993 finish = progress = result = None
995 if progress is not None:
996 progress.hide()
998 if result is not None:
999 result(task.result)
1001 if finish is not None:
1002 finish(task)
1005 # Syntax highlighting
1007 def rgb(r, g, b):
1008 color = QtGui.QColor()
1009 color.setRgb(r, g, b)
1010 return color
1013 def rgba(r, g, b, a=255):
1014 color = rgb(r, g, b)
1015 color.setAlpha(a)
1016 return color
1019 def RGB(args):
1020 return rgb(*args)
1023 def rgb_css(color):
1024 """Convert a QColor into an rgb(int, int, int) CSS string"""
1025 return 'rgb(%d, %d, %d)' % (color.red(), color.green(), color.blue())
1028 def rgb_hex(color):
1029 """Convert a QColor into a hex aabbcc string"""
1030 return '%02x%02x%02x' % (color.red(), color.green(), color.blue())
1033 def hsl(h, s, l):
1034 return QtGui.QColor.fromHslF(
1035 utils.clamp(h, 0.0, 1.0),
1036 utils.clamp(s, 0.0, 1.0),
1037 utils.clamp(l, 0.0, 1.0)
1041 def hsl_css(h, s, l):
1042 return rgb_css(hsl(h, s, l))
1045 def make_format(fg=None, bg=None, bold=False):
1046 fmt = QtGui.QTextCharFormat()
1047 if fg:
1048 fmt.setForeground(fg)
1049 if bg:
1050 fmt.setBackground(bg)
1051 if bold:
1052 fmt.setFontWeight(QtGui.QFont.Bold)
1053 return fmt
1056 class ImageFormats(object):
1058 def __init__(self):
1059 # returns a list of QByteArray objects
1060 formats_qba = QtGui.QImageReader.supportedImageFormats()
1061 # portability: python3 data() returns bytes, python2 returns str
1062 decode = core.decode
1063 formats = [decode(x.data()) for x in formats_qba]
1064 self.extensions = set(['.' + fmt for fmt in formats])
1066 def ok(self, filename):
1067 _, ext = os.path.splitext(filename)
1068 return ext.lower() in self.extensions