qtutils: use a read-only directory chooser
[git-cola.git] / cola / qtutils.py
blob7664e32406f9c195c25a104a0620241ee5a035a0
1 """Miscellaneous Qt utility functions."""
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 import os
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 hotkeys
14 from . import icons
15 from . import utils
16 from .i18n import N_
17 from .compat import int_types
18 from .compat import ustr
19 from .models import prefs
20 from .widgets import defs
23 STRETCH = object()
24 SKIPPED = object()
27 def active_window():
28 """Return the active window for the current application"""
29 return QtWidgets.QApplication.activeWindow()
32 def connect_action(action, fn):
33 """Connect an action to a function"""
34 action.triggered[bool].connect(lambda x: fn())
37 def connect_action_bool(action, fn):
38 """Connect a triggered(bool) action to a function"""
39 action.triggered[bool].connect(fn)
42 def connect_button(button, fn):
43 """Connect a button to a function"""
44 # Some versions of Qt send the `bool` argument to the clicked callback,
45 # and some do not. The lambda consumes all callback-provided arguments.
46 button.clicked.connect(lambda *args, **kwargs: fn())
49 def connect_checkbox(widget, fn):
50 """Connect a checkbox to a function taking bool"""
51 widget.clicked.connect(lambda *args, **kwargs: fn(get(checkbox)))
54 def connect_released(button, fn):
55 """Connect a button to a function"""
56 button.released.connect(fn)
59 def button_action(button, action):
60 """Make a button trigger an action"""
61 connect_button(button, action.trigger)
64 def connect_toggle(toggle, fn):
65 """Connect a toggle button to a function"""
66 toggle.toggled.connect(fn)
69 def disconnect(signal):
70 """Disconnect signal from all slots"""
71 try:
72 signal.disconnect()
73 except TypeError: # allow unconnected slots
74 pass
77 def get(widget):
78 """Query a widget for its python value"""
79 if hasattr(widget, 'isChecked'):
80 value = widget.isChecked()
81 elif hasattr(widget, 'value'):
82 value = widget.value()
83 elif hasattr(widget, 'text'):
84 value = widget.text()
85 elif hasattr(widget, 'toPlainText'):
86 value = widget.toPlainText()
87 elif hasattr(widget, 'sizes'):
88 value = widget.sizes()
89 elif hasattr(widget, 'date'):
90 value = widget.date().toString(Qt.ISODate)
91 else:
92 value = None
93 return value
96 def hbox(margin, spacing, *items):
97 """Create an HBoxLayout with the specified sizes and items"""
98 return box(QtWidgets.QHBoxLayout, margin, spacing, *items)
101 def vbox(margin, spacing, *items):
102 """Create a VBoxLayout with the specified sizes and items"""
103 return box(QtWidgets.QVBoxLayout, margin, spacing, *items)
106 def buttongroup(*items):
107 """Create a QButtonGroup for the specified items"""
108 group = QtWidgets.QButtonGroup()
109 for i in items:
110 group.addButton(i)
111 return group
114 def set_margin(layout, margin):
115 """Set the content margins for a layout"""
116 layout.setContentsMargins(margin, margin, margin, margin)
119 def box(cls, margin, spacing, *items):
120 """Create a QBoxLayout with the specified sizes and items"""
121 stretch = STRETCH
122 skipped = SKIPPED
123 layout = cls()
124 layout.setSpacing(spacing)
125 set_margin(layout, margin)
127 for i in items:
128 if isinstance(i, QtWidgets.QWidget):
129 layout.addWidget(i)
130 elif isinstance(
133 QtWidgets.QHBoxLayout,
134 QtWidgets.QVBoxLayout,
135 QtWidgets.QFormLayout,
136 QtWidgets.QLayout,
139 layout.addLayout(i)
140 elif i is stretch:
141 layout.addStretch()
142 elif i is skipped:
143 continue
144 elif isinstance(i, int_types):
145 layout.addSpacing(i)
147 return layout
150 def form(margin, spacing, *widgets):
151 """Create a QFormLayout with the specified sizes and items"""
152 layout = QtWidgets.QFormLayout()
153 layout.setSpacing(spacing)
154 layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
155 set_margin(layout, margin)
157 for idx, (name, widget) in enumerate(widgets):
158 if isinstance(name, (str, ustr)):
159 layout.addRow(name, widget)
160 else:
161 layout.setWidget(idx, QtWidgets.QFormLayout.LabelRole, name)
162 layout.setWidget(idx, QtWidgets.QFormLayout.FieldRole, widget)
164 return layout
167 def grid(margin, spacing, *widgets):
168 """Create a QGridLayout with the specified sizes and items"""
169 layout = QtWidgets.QGridLayout()
170 layout.setSpacing(spacing)
171 set_margin(layout, margin)
173 for row in widgets:
174 item = row[0]
175 if isinstance(item, QtWidgets.QWidget):
176 layout.addWidget(*row)
177 elif isinstance(item, QtWidgets.QLayoutItem):
178 layout.addItem(*row)
180 return layout
183 def splitter(orientation, *widgets):
184 """Create a spliter over the specified widgets
186 :param orientation: Qt.Horizontal or Qt.Vertical
189 layout = QtWidgets.QSplitter()
190 layout.setOrientation(orientation)
191 layout.setHandleWidth(defs.handle_width)
192 layout.setChildrenCollapsible(True)
194 for idx, widget in enumerate(widgets):
195 layout.addWidget(widget)
196 layout.setStretchFactor(idx, 1)
198 # Workaround for Qt not setting the WA_Hover property for QSplitter
199 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
200 layout.handle(1).setAttribute(Qt.WA_Hover)
202 return layout
205 def label(text=None, align=None, fmt=None, selectable=True):
206 """Create a QLabel with the specified properties"""
207 widget = QtWidgets.QLabel()
208 if align is not None:
209 widget.setAlignment(align)
210 if fmt is not None:
211 widget.setTextFormat(fmt)
212 if selectable:
213 widget.setTextInteractionFlags(Qt.TextBrowserInteraction)
214 widget.setOpenExternalLinks(True)
215 if text:
216 widget.setText(text)
217 return widget
220 class ComboBox(QtWidgets.QComboBox):
221 """Custom read-only combobox with a convenient API"""
223 def __init__(self, items=None, editable=False, parent=None, transform=None):
224 super(ComboBox, self).__init__(parent)
225 self.setEditable(editable)
226 self.transform = transform
227 self.item_data = []
228 if items:
229 self.addItems(items)
230 self.item_data.extend(items)
232 def set_index(self, idx):
233 idx = utils.clamp(idx, 0, self.count() - 1)
234 self.setCurrentIndex(idx)
236 def add_item(self, text, data):
237 self.addItem(text)
238 self.item_data.append(data)
240 def current_data(self):
241 return self.item_data[self.currentIndex()]
243 def set_value(self, value):
244 if self.transform:
245 value = self.transform(value)
246 try:
247 index = self.item_data.index(value)
248 except ValueError:
249 index = 0
250 self.setCurrentIndex(index)
253 def combo(items, editable=False, parent=None):
254 """Create a readonly (by default) combobox from a list of items"""
255 return ComboBox(editable=editable, items=items, parent=parent)
258 def combo_mapped(data, editable=False, transform=None, parent=None):
259 """Create a readonly (by default) combobox from a list of items"""
260 widget = ComboBox(editable=editable, transform=transform, parent=parent)
261 for (k, v) in data:
262 widget.add_item(k, v)
263 return widget
266 def textbrowser(text=None):
267 """Create a QTextBrowser for the specified text"""
268 widget = QtWidgets.QTextBrowser()
269 widget.setOpenExternalLinks(True)
270 if text:
271 widget.setText(text)
272 return widget
275 def add_completer(widget, items):
276 """Add simple completion to a widget"""
277 completer = QtWidgets.QCompleter(items, widget)
278 completer.setCaseSensitivity(Qt.CaseInsensitive)
279 completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion)
280 widget.setCompleter(completer)
283 def prompt(msg, title=None, text='', parent=None):
284 """Presents the user with an input widget and returns the input."""
285 if title is None:
286 title = msg
287 if parent is None:
288 parent = active_window()
289 result = QtWidgets.QInputDialog.getText(
290 parent, title, msg, QtWidgets.QLineEdit.Normal, text
292 return (result[0], result[1])
295 def prompt_n(msg, inputs):
296 """Presents the user with N input widgets and returns the results"""
297 dialog = QtWidgets.QDialog(active_window())
298 dialog.setWindowModality(Qt.WindowModal)
299 dialog.setWindowTitle(msg)
301 long_value = msg
302 for k, v in inputs:
303 if len(k + v) > len(long_value):
304 long_value = k + v
306 metrics = QtGui.QFontMetrics(dialog.font())
307 min_width = metrics.width(long_value) + 100
308 if min_width > 720:
309 min_width = 720
310 dialog.setMinimumWidth(min_width)
312 ok_b = ok_button(msg, enabled=False)
313 close_b = close_button()
315 form_widgets = []
317 def get_values():
318 return [pair[1].text().strip() for pair in form_widgets]
320 for name, value in inputs:
321 lineedit = QtWidgets.QLineEdit()
322 # Enable the OK button only when all fields have been populated
323 # pylint: disable=no-member
324 lineedit.textChanged.connect(lambda x: ok_b.setEnabled(all(get_values())))
325 if value:
326 lineedit.setText(value)
327 form_widgets.append((name, lineedit))
329 # layouts
330 form_layout = form(defs.no_margin, defs.button_spacing, *form_widgets)
331 button_layout = hbox(defs.no_margin, defs.button_spacing, STRETCH, close_b, ok_b)
332 main_layout = vbox(defs.margin, defs.button_spacing, form_layout, button_layout)
333 dialog.setLayout(main_layout)
335 # connections
336 connect_button(ok_b, dialog.accept)
337 connect_button(close_b, dialog.reject)
339 accepted = dialog.exec_() == QtWidgets.QDialog.Accepted
340 text = get_values()
341 ok = accepted and all(text)
342 return (ok, text)
345 class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
347 TYPE = QtGui.QStandardItem.UserType + 101
349 def __init__(self, path, icon, deleted):
350 QtWidgets.QTreeWidgetItem.__init__(self)
351 self.path = path
352 self.deleted = deleted
353 self.setIcon(0, icons.from_name(icon))
354 self.setText(0, path)
356 def type(self):
357 return self.TYPE
360 def paths_from_indexes(model, indexes, item_type=TreeWidgetItem.TYPE, item_filter=None):
361 """Return paths from a list of QStandardItemModel indexes"""
362 items = [model.itemFromIndex(i) for i in indexes]
363 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
366 def _true_filter(_x):
367 return True
370 def paths_from_items(items, item_type=TreeWidgetItem.TYPE, 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 if i.type() == item_type and item_filter(i)]
377 def tree_selection(tree_item, items):
378 """Returns an array of model items that correspond to the selected
379 QTreeWidgetItem children"""
380 selected = []
381 count = min(tree_item.childCount(), len(items))
382 for idx in range(count):
383 if tree_item.child(idx).isSelected():
384 selected.append(items[idx])
386 return selected
389 def tree_selection_items(tree_item):
390 """Returns selected widget items"""
391 selected = []
392 for idx in range(tree_item.childCount()):
393 child = tree_item.child(idx)
394 if child.isSelected():
395 selected.append(child)
397 return selected
400 def selected_item(list_widget, items):
401 """Returns the model item that corresponds to the selected QListWidget
402 row."""
403 widget_items = list_widget.selectedItems()
404 if not widget_items:
405 return None
406 widget_item = widget_items[0]
407 row = list_widget.row(widget_item)
408 if row < len(items):
409 item = items[row]
410 else:
411 item = None
412 return item
415 def selected_items(list_widget, items):
416 """Returns an array of model items that correspond to the selected
417 QListWidget rows."""
418 item_count = len(items)
419 selected = []
420 for widget_item in list_widget.selectedItems():
421 row = list_widget.row(widget_item)
422 if row < item_count:
423 selected.append(items[row])
424 return selected
427 def open_file(title, directory=None):
428 """Creates an Open File dialog and returns a filename."""
429 result = compat.getopenfilename(
430 parent=active_window(), caption=title, basedir=directory
432 return result[0]
435 def open_files(title, directory=None, filters=''):
436 """Creates an Open File dialog and returns a list of filenames."""
437 result = compat.getopenfilenames(
438 parent=active_window(), caption=title, basedir=directory, filters=filters
440 return result[0]
443 def opendir_dialog(caption, path):
444 """Prompts for a directory path"""
445 options = (
446 QtWidgets.QFileDialog.Directory |
447 QtWidgets.QFileDialog.DontResolveSymlinks |
448 QtWidgets.QFileDialog.ReadOnly |
449 QtWidgets.QFileDialog.ShowDirsOnly
451 return compat.getexistingdirectory(
452 parent=active_window(), caption=caption, basedir=path, options=options
456 def save_as(filename, title='Save As...'):
457 """Creates a Save File dialog and returns a filename."""
458 result = compat.getsavefilename(
459 parent=active_window(), caption=title, 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 if not utils.is_darwin() and not utils.is_win32():
480 clipboard.setText(text, QtGui.QClipboard.Selection)
481 persist_clipboard()
484 # pylint: disable=line-too-long
485 def persist_clipboard():
486 """Persist the clipboard
488 X11 stores only a reference to the clipboard data.
489 Send a clipboard event to force a copy of the clipboard to occur.
490 This ensures that the clipboard is present after git-cola exits.
491 Otherwise, the reference is destroyed on exit.
493 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
495 """ # noqa
496 clipboard = QtWidgets.QApplication.clipboard()
497 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
498 QtWidgets.QApplication.sendEvent(clipboard, event)
501 def add_action_bool(widget, text, fn, checked, *shortcuts):
502 tip = text
503 action = _add_action(widget, text, tip, fn, 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 icon = icons.name_from_basename(icon_name)
564 return TreeWidgetItem(filename, icon, deleted=deleted)
567 def add_close_action(widget):
568 """Adds close action and shortcuts to a widget."""
569 return add_action(widget, N_('Close...'), widget.close, hotkeys.CLOSE, hotkeys.QUIT)
572 def app():
573 """Return the current application"""
574 return QtWidgets.QApplication.instance()
577 def desktop():
578 """Return the desktop"""
579 return app().desktop()
582 def desktop_size():
583 desk = desktop()
584 rect = desk.screenGeometry(QtGui.QCursor().pos())
585 return (rect.width(), rect.height())
588 def center_on_screen(widget):
589 """Move widget to the center of the default screen"""
590 width, height = desktop_size()
591 cx = width // 2
592 cy = height // 2
593 widget.move(cx - widget.width() // 2, cy - widget.height() // 2)
596 def default_size(parent, width, height, use_parent_height=True):
597 """Return the parent's size, or the provided defaults"""
598 if parent is not None:
599 width = parent.width()
600 if use_parent_height:
601 height = parent.height()
602 return (width, height)
605 def default_monospace_font():
606 if utils.is_darwin():
607 family = 'Monaco'
608 else:
609 family = 'Monospace'
610 mfont = QtGui.QFont()
611 mfont.setFamily(family)
612 return mfont
615 def diff_font_str(context):
616 cfg = context.cfg
617 font_str = cfg.get(prefs.FONTDIFF)
618 if not font_str:
619 font_str = default_monospace_font().toString()
620 return font_str
623 def diff_font(context):
624 return font(diff_font_str(context))
627 def font(string):
628 qfont = QtGui.QFont()
629 qfont.fromString(string)
630 return qfont
633 def create_button(
634 text='', layout=None, tooltip=None, icon=None, 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(
669 /* No borders */
670 QToolButton {
671 border: none;
672 background-color: none;
674 /* Hide the menu indicator */
675 QToolButton::menu-indicator {
676 image: none;
678 QToolButton:hover {
679 border: %(border)spx solid %(highlight_rgb)s;
682 % dict(border=defs.border, highlight_rgb=highlight_rgb)
684 return button
687 def create_action_button(tooltip=None, icon=None):
688 """Create a small toolbutton for use in dock title widgets"""
689 button = tool_button()
690 if tooltip is not None:
691 button.setToolTip(tooltip)
692 if icon is not None:
693 button.setIcon(icon)
694 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
695 return button
698 def ok_button(text, default=True, enabled=True, icon=None):
699 if icon is None:
700 icon = icons.ok()
701 return create_button(text=text, icon=icon, default=default, enabled=enabled)
704 def close_button(text=None, icon=None):
705 text = text or N_('Close')
706 icon = icons.mkicon(icon, icons.close)
707 return create_button(text=text, icon=icon)
710 def edit_button(enabled=True, default=False):
711 return create_button(
712 text=N_('Edit'), icon=icons.edit(), enabled=enabled, default=default
716 def refresh_button(enabled=True, default=False):
717 return create_button(
718 text=N_('Refresh'), icon=icons.sync(), enabled=enabled, default=default
722 def checkbox(text='', tooltip='', checked=None):
723 """Create a checkbox"""
724 return _checkbox(QtWidgets.QCheckBox, text, tooltip, checked)
727 def radio(text='', tooltip='', checked=None):
728 """Create a radio button"""
729 return _checkbox(QtWidgets.QRadioButton, text, tooltip, checked)
732 def _checkbox(cls, text, tooltip, checked):
733 """Create a widget and apply properties"""
734 widget = cls()
735 if text:
736 widget.setText(text)
737 if tooltip:
738 widget.setToolTip(tooltip)
739 if checked is not None:
740 widget.setChecked(checked)
741 return widget
744 class DockTitleBarWidget(QtWidgets.QFrame):
745 def __init__(self, parent, title, stretch=True):
746 QtWidgets.QFrame.__init__(self, parent)
747 self.setAutoFillBackground(True)
748 self.label = qlabel = QtWidgets.QLabel(title, self)
749 qfont = qlabel.font()
750 qfont.setBold(True)
751 qlabel.setFont(qfont)
752 qlabel.setCursor(Qt.OpenHandCursor)
754 self.close_button = create_action_button(
755 tooltip=N_('Close'), icon=icons.close()
758 self.toggle_button = create_action_button(
759 tooltip=N_('Detach'), icon=icons.external()
762 self.corner_layout = hbox(defs.no_margin, defs.spacing)
764 if stretch:
765 separator = STRETCH
766 else:
767 separator = SKIPPED
769 self.main_layout = hbox(
770 defs.small_margin,
771 defs.titlebar_spacing,
772 qlabel,
773 separator,
774 self.corner_layout,
775 self.toggle_button,
776 self.close_button,
778 self.setLayout(self.main_layout)
780 connect_button(self.toggle_button, self.toggle_floating)
781 connect_button(self.close_button, self.toggle_visibility)
783 def toggle_floating(self):
784 self.parent().setFloating(not self.parent().isFloating())
785 self.update_tooltips()
787 def toggle_visibility(self):
788 self.parent().toggleViewAction().trigger()
790 def set_title(self, title):
791 self.label.setText(title)
793 def add_corner_widget(self, widget):
794 self.corner_layout.addWidget(widget)
796 def update_tooltips(self):
797 if self.parent().isFloating():
798 tooltip = N_('Attach')
799 else:
800 tooltip = N_('Detach')
801 self.toggle_button.setToolTip(tooltip)
804 def create_dock(name, title, parent, stretch=True, widget=None, fn=None):
805 """Create a dock widget and set it up accordingly."""
806 dock = QtWidgets.QDockWidget(parent)
807 dock.setWindowTitle(title)
808 dock.setObjectName(name)
809 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
810 dock.setTitleBarWidget(titlebar)
811 dock.setAutoFillBackground(True)
812 if hasattr(parent, 'dockwidgets'):
813 parent.dockwidgets.append(dock)
814 if fn:
815 widget = fn(dock)
816 assert isinstance(widget, QtWidgets.QFrame), "Docked widget has to be a QFrame"
817 if widget:
818 dock.setWidget(widget)
819 return dock
822 def hide_dock(widget):
823 widget.toggleViewAction().setChecked(False)
824 widget.hide()
827 def create_menu(title, parent):
828 """Create a menu and set its title."""
829 qmenu = DebouncingMenu(title, parent)
830 return qmenu
833 class DebouncingMenu(QtWidgets.QMenu):
834 """Menu that debounces mouse release action ie. stops it if occurred
835 right after menu creation.
837 Disables annoying behaviour when RMB is pressed to show menu, cursor is
838 moved accidentally 1px onto newly created menu and released causing to
839 execute menu action
842 threshold_ms = 400
844 def __init__(self, title, parent):
845 QtWidgets.QMenu.__init__(self, title, parent)
846 self.created_at = utils.epoch_millis()
847 if hasattr(self, 'setToolTipsVisible'):
848 self.setToolTipsVisible(True)
850 def mouseReleaseEvent(self, event):
851 threshold = DebouncingMenu.threshold_ms
852 if (utils.epoch_millis() - self.created_at) > threshold:
853 QtWidgets.QMenu.mouseReleaseEvent(self, event)
856 def add_menu(title, parent):
857 """Create a menu and set its title."""
858 menu = create_menu(title, parent)
859 if hasattr(parent, 'addMenu'):
860 parent.addMenu(menu)
861 else:
862 parent.addAction(menu.menuAction())
863 return menu
866 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
867 button = tool_button()
868 if icon is not None:
869 button.setIcon(icon)
870 button.setIconSize(QtCore.QSize(defs.default_icon, defs.default_icon))
871 if text is not None:
872 button.setText(' ' + text)
873 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
874 if tooltip is not None:
875 button.setToolTip(tooltip)
876 if layout is not None:
877 layout.addWidget(button)
878 return button
881 # pylint: disable=line-too-long
882 def mimedata_from_paths(context, paths):
883 """Return mimedata with a list of absolute path URLs
885 The text/x-moz-list format is always included by Qt, and doing
886 mimedata.removeFormat('text/x-moz-url') has no effect.
887 C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
889 gnome-terminal expects utf-16 encoded text, but other terminals,
890 e.g. terminator, prefer utf-8, so allow cola.dragencoding
891 to override the default.
893 """ # noqa
894 cfg = context.cfg
895 abspaths = [core.abspath(path) for path in paths]
896 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
898 mimedata = QtCore.QMimeData()
899 mimedata.setUrls(urls)
901 paths_text = core.list2cmdline(abspaths)
902 encoding = cfg.get('cola.dragencoding', 'utf-16')
903 moz_text = core.encode(paths_text, encoding=encoding)
904 mimedata.setData('text/x-moz-url', moz_text)
906 return mimedata
909 def path_mimetypes():
910 return ['text/uri-list', 'text/x-moz-url']
913 class BlockSignals(object):
914 """Context manager for blocking a signals on a widget"""
916 def __init__(self, *widgets):
917 self.widgets = widgets
918 self.values = []
920 def __enter__(self):
921 """Block Qt signals for all of the captured widgets"""
922 self.values = [widget.blockSignals(True) for widget in self.widgets]
923 return self
925 def __exit__(self, exc_type, exc_val, exc_tb):
926 """Restore Qt signals when we exit the scope"""
927 for (widget, value) in zip(self.widgets, self.values):
928 widget.blockSignals(value)
931 class Channel(QtCore.QObject):
932 finished = Signal(object)
933 result = Signal(object)
936 class Task(QtCore.QRunnable):
937 """Disable auto-deletion to avoid gc issues
939 Python's garbage collector will try to double-free the task
940 once it's finished, so disable Qt's auto-deletion as a workaround.
944 def __init__(self):
945 QtCore.QRunnable.__init__(self)
947 self.channel = Channel()
948 self.result = None
949 self.setAutoDelete(False)
951 def run(self):
952 self.result = self.task()
953 self.channel.result.emit(self.result)
954 self.channel.finished.emit(self)
956 # pylint: disable=no-self-use
957 def task(self):
958 return None
960 def connect(self, handler):
961 self.channel.result.connect(handler, type=Qt.QueuedConnection)
964 class SimpleTask(Task):
965 """Run a simple callable as a task"""
967 def __init__(self, fn, *args, **kwargs):
968 Task.__init__(self)
970 self.fn = fn
971 self.args = args
972 self.kwargs = kwargs
974 def task(self):
975 return self.fn(*self.args, **self.kwargs)
978 class RunTask(QtCore.QObject):
979 """Runs QRunnable instances and transfers control when they finish"""
981 def __init__(self, parent=None):
982 QtCore.QObject.__init__(self, parent)
983 self.tasks = []
984 self.task_details = {}
985 self.threadpool = QtCore.QThreadPool.globalInstance()
986 self.result_fn = None
988 def start(self, task, progress=None, finish=None, result=None):
989 """Start the task and register a callback"""
990 self.result_fn = result
991 if progress is not None:
992 progress.show()
993 # prevents garbage collection bugs in certain PyQt4 versions
994 self.tasks.append(task)
995 task_id = id(task)
996 self.task_details[task_id] = (progress, finish, result)
997 task.channel.finished.connect(self.finish, type=Qt.QueuedConnection)
998 self.threadpool.start(task)
1000 def finish(self, task):
1001 task_id = id(task)
1002 try:
1003 self.tasks.remove(task)
1004 except ValueError:
1005 pass
1006 try:
1007 progress, finish, result = self.task_details[task_id]
1008 del self.task_details[task_id]
1009 except KeyError:
1010 finish = progress = result = None
1012 if progress is not None:
1013 progress.hide()
1015 if result is not None:
1016 result(task.result)
1018 if finish is not None:
1019 finish(task)
1022 # Syntax highlighting
1025 def rgb(r, g, b):
1026 color = QtGui.QColor()
1027 color.setRgb(r, g, b)
1028 return color
1031 def rgba(r, g, b, a=255):
1032 color = rgb(r, g, b)
1033 color.setAlpha(a)
1034 return color
1037 def RGB(args):
1038 return rgb(*args)
1041 def rgb_css(color):
1042 """Convert a QColor into an rgb(int, int, int) CSS string"""
1043 return 'rgb(%d, %d, %d)' % (color.red(), color.green(), color.blue())
1046 def rgb_hex(color):
1047 """Convert a QColor into a hex aabbcc string"""
1048 return '%02x%02x%02x' % (color.red(), color.green(), color.blue())
1051 def hsl(h, s, light):
1052 return QtGui.QColor.fromHslF(
1053 utils.clamp(h, 0.0, 1.0), utils.clamp(s, 0.0, 1.0), utils.clamp(light, 0.0, 1.0)
1057 def hsl_css(h, s, light):
1058 return rgb_css(hsl(h, s, light))
1061 def make_format(fg=None, bg=None, bold=False):
1062 fmt = QtGui.QTextCharFormat()
1063 if fg:
1064 fmt.setForeground(fg)
1065 if bg:
1066 fmt.setBackground(bg)
1067 if bold:
1068 fmt.setFontWeight(QtGui.QFont.Bold)
1069 return fmt
1072 class ImageFormats(object):
1073 def __init__(self):
1074 # returns a list of QByteArray objects
1075 formats_qba = QtGui.QImageReader.supportedImageFormats()
1076 # portability: python3 data() returns bytes, python2 returns str
1077 decode = core.decode
1078 formats = [decode(x.data()) for x in formats_qba]
1079 self.extensions = set(['.' + fmt for fmt in formats])
1081 def ok(self, filename):
1082 _, ext = os.path.splitext(filename)
1083 return ext.lower() in self.extensions
1086 def set_scrollbar_values(widget, hscroll_value, vscroll_value):
1087 """Set scrollbars to the specified values"""
1088 hscroll = widget.horizontalScrollBar()
1089 if hscroll and hscroll_value is not None:
1090 hscroll.setValue(hscroll_value)
1092 vscroll = widget.verticalScrollBar()
1093 if vscroll and vscroll_value is not None:
1094 vscroll.setValue(vscroll_value)
1097 def get_scrollbar_values(widget):
1098 """Return the current (hscroll, vscroll) scrollbar values for a widget"""
1099 hscroll = widget.horizontalScrollBar()
1100 if hscroll:
1101 hscroll_value = get(hscroll)
1102 else:
1103 hscroll_value = None
1104 vscroll = widget.verticalScrollBar()
1105 if vscroll:
1106 vscroll_value = get(vscroll)
1107 else:
1108 vscroll_value = None
1109 return (hscroll_value, vscroll_value)
1112 def scroll_to_item(widget, item):
1113 """Scroll to an item while retaining the horizontal scroll position"""
1114 hscroll = None
1115 hscrollbar = widget.horizontalScrollBar()
1116 if hscrollbar:
1117 hscroll = get(hscrollbar)
1118 widget.scrollToItem(item)
1119 if hscroll is not None:
1120 hscrollbar.setValue(hscroll)
1123 def select_item(widget, item):
1124 """Scroll to and make a QTreeWidget item selected and current"""
1125 scroll_to_item(widget, item)
1126 widget.setCurrentItem(item)
1127 item.setSelected(True)
1130 def get_selected_values(widget, top_level_idx, values):
1131 """Map the selected items under the top-level item to the values list"""
1132 # Get the top-level item
1133 item = widget.topLevelItem(top_level_idx)
1134 return tree_selection(item, values)
1137 def get_selected_items(widget, idx):
1138 """Return the selected items under the top-level item"""
1139 item = widget.topLevelItem(idx)
1140 return tree_selection_items(item)