pylint: rename variables for readability
[git-cola.git] / cola / qtutils.py
blob7969d3b6617b6cc5007f379d51906f66ef552453
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 current_palette():
33 """Return the QPalette for the current application"""
34 return QtWidgets.QApplication.instance().palette()
37 def connect_action(action, func):
38 """Connect an action to a function"""
39 action.triggered[bool].connect(lambda x: func(), type=Qt.QueuedConnection)
42 def connect_action_bool(action, func):
43 """Connect a triggered(bool) action to a function"""
44 action.triggered[bool].connect(func, type=Qt.QueuedConnection)
47 def connect_button(button, func):
48 """Connect a button to a function"""
49 # Some versions of Qt send the `bool` argument to the clicked callback,
50 # and some do not. The lambda consumes all callback-provided arguments.
51 button.clicked.connect(lambda *args, **kwargs: func(), type=Qt.QueuedConnection)
54 def connect_checkbox(widget, func):
55 """Connect a checkbox to a function taking bool"""
56 widget.clicked.connect(
57 lambda *args, **kwargs: func(get(checkbox)), type=Qt.QueuedConnection
61 def connect_released(button, func):
62 """Connect a button to a function"""
63 button.released.connect(func, type=Qt.QueuedConnection)
66 def button_action(button, action):
67 """Make a button trigger an action"""
68 connect_button(button, action.trigger)
71 def connect_toggle(toggle, func):
72 """Connect a toggle button to a function"""
73 toggle.toggled.connect(func, type=Qt.QueuedConnection)
76 def disconnect(signal):
77 """Disconnect signal from all slots"""
78 try:
79 signal.disconnect()
80 except TypeError: # allow unconnected slots
81 pass
84 def get(widget, default=None):
85 """Query a widget for its python value"""
86 if hasattr(widget, 'isChecked'):
87 value = widget.isChecked()
88 elif hasattr(widget, 'value'):
89 value = widget.value()
90 elif hasattr(widget, 'text'):
91 value = widget.text()
92 elif hasattr(widget, 'toPlainText'):
93 value = widget.toPlainText()
94 elif hasattr(widget, 'sizes'):
95 value = widget.sizes()
96 elif hasattr(widget, 'date'):
97 value = widget.date().toString(Qt.ISODate)
98 else:
99 value = default
100 return value
103 def hbox(margin, spacing, *items):
104 """Create an HBoxLayout with the specified sizes and items"""
105 return box(QtWidgets.QHBoxLayout, margin, spacing, *items)
108 def vbox(margin, spacing, *items):
109 """Create a VBoxLayout with the specified sizes and items"""
110 return box(QtWidgets.QVBoxLayout, margin, spacing, *items)
113 def buttongroup(*items):
114 """Create a QButtonGroup for the specified items"""
115 group = QtWidgets.QButtonGroup()
116 for i in items:
117 group.addButton(i)
118 return group
121 def set_margin(layout, margin):
122 """Set the content margins for a layout"""
123 layout.setContentsMargins(margin, margin, margin, margin)
126 def box(cls, margin, spacing, *items):
127 """Create a QBoxLayout with the specified sizes and items"""
128 stretch = STRETCH
129 skipped = SKIPPED
130 layout = cls()
131 layout.setSpacing(spacing)
132 set_margin(layout, margin)
134 for i in items:
135 if isinstance(i, QtWidgets.QWidget):
136 layout.addWidget(i)
137 elif isinstance(
140 QtWidgets.QHBoxLayout,
141 QtWidgets.QVBoxLayout,
142 QtWidgets.QFormLayout,
143 QtWidgets.QLayout,
146 layout.addLayout(i)
147 elif i is stretch:
148 layout.addStretch()
149 elif i is skipped:
150 continue
151 elif isinstance(i, int_types):
152 layout.addSpacing(i)
154 return layout
157 def form(margin, spacing, *widgets):
158 """Create a QFormLayout with the specified sizes and items"""
159 layout = QtWidgets.QFormLayout()
160 layout.setSpacing(spacing)
161 layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
162 set_margin(layout, margin)
164 for idx, (name, widget) in enumerate(widgets):
165 if isinstance(name, (str, ustr)):
166 layout.addRow(name, widget)
167 else:
168 layout.setWidget(idx, QtWidgets.QFormLayout.LabelRole, name)
169 layout.setWidget(idx, QtWidgets.QFormLayout.FieldRole, widget)
171 return layout
174 def grid(margin, spacing, *widgets):
175 """Create a QGridLayout with the specified sizes and items"""
176 layout = QtWidgets.QGridLayout()
177 layout.setSpacing(spacing)
178 set_margin(layout, margin)
180 for row in widgets:
181 item = row[0]
182 if isinstance(item, QtWidgets.QWidget):
183 layout.addWidget(*row)
184 elif isinstance(item, QtWidgets.QLayoutItem):
185 layout.addItem(*row)
187 return layout
190 def splitter(orientation, *widgets):
191 """Create a spliter over the specified widgets
193 :param orientation: Qt.Horizontal or Qt.Vertical
196 layout = QtWidgets.QSplitter()
197 layout.setOrientation(orientation)
198 layout.setHandleWidth(defs.handle_width)
199 layout.setChildrenCollapsible(True)
201 for idx, widget in enumerate(widgets):
202 layout.addWidget(widget)
203 layout.setStretchFactor(idx, 1)
205 # Workaround for Qt not setting the WA_Hover property for QSplitter
206 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
207 layout.handle(1).setAttribute(Qt.WA_Hover)
209 return layout
212 def label(text=None, align=None, fmt=None, selectable=True):
213 """Create a QLabel with the specified properties"""
214 widget = QtWidgets.QLabel()
215 if align is not None:
216 widget.setAlignment(align)
217 if fmt is not None:
218 widget.setTextFormat(fmt)
219 if selectable:
220 widget.setTextInteractionFlags(Qt.TextBrowserInteraction)
221 widget.setOpenExternalLinks(True)
222 if text:
223 widget.setText(text)
224 return widget
227 class ComboBox(QtWidgets.QComboBox):
228 """Custom read-only combobox with a convenient API"""
230 def __init__(self, items=None, editable=False, parent=None, transform=None):
231 super(ComboBox, self).__init__(parent)
232 self.setEditable(editable)
233 self.transform = transform
234 self.item_data = []
235 if items:
236 self.addItems(items)
237 self.item_data.extend(items)
239 def set_index(self, idx):
240 idx = utils.clamp(idx, 0, self.count() - 1)
241 self.setCurrentIndex(idx)
243 def add_item(self, text, data):
244 self.addItem(text)
245 self.item_data.append(data)
247 def current_data(self):
248 return self.item_data[self.currentIndex()]
250 def set_value(self, value):
251 if self.transform:
252 value = self.transform(value)
253 try:
254 index = self.item_data.index(value)
255 except ValueError:
256 index = 0
257 self.setCurrentIndex(index)
260 def combo(items, editable=False, parent=None):
261 """Create a readonly (by default) combobox from a list of items"""
262 return ComboBox(editable=editable, items=items, parent=parent)
265 def combo_mapped(data, editable=False, transform=None, parent=None):
266 """Create a readonly (by default) combobox from a list of items"""
267 widget = ComboBox(editable=editable, transform=transform, parent=parent)
268 for (k, v) in data:
269 widget.add_item(k, v)
270 return widget
273 def textbrowser(text=None):
274 """Create a QTextBrowser for the specified text"""
275 widget = QtWidgets.QTextBrowser()
276 widget.setOpenExternalLinks(True)
277 if text:
278 widget.setText(text)
279 return widget
282 def add_completer(widget, items):
283 """Add simple completion to a widget"""
284 completer = QtWidgets.QCompleter(items, widget)
285 completer.setCaseSensitivity(Qt.CaseInsensitive)
286 completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion)
287 widget.setCompleter(completer)
290 def prompt(msg, title=None, text='', parent=None):
291 """Presents the user with an input widget and returns the input."""
292 if title is None:
293 title = msg
294 if parent is None:
295 parent = active_window()
296 result = QtWidgets.QInputDialog.getText(
297 parent, title, msg, QtWidgets.QLineEdit.Normal, text
299 return (result[0], result[1])
302 def prompt_n(msg, inputs):
303 """Presents the user with N input widgets and returns the results"""
304 dialog = QtWidgets.QDialog(active_window())
305 dialog.setWindowModality(Qt.WindowModal)
306 dialog.setWindowTitle(msg)
308 long_value = msg
309 for k, v in inputs:
310 if len(k + v) > len(long_value):
311 long_value = k + v
313 metrics = QtGui.QFontMetrics(dialog.font())
314 min_width = min(720, metrics.width(long_value) + 100)
315 dialog.setMinimumWidth(min_width)
317 ok_b = ok_button(msg, enabled=False)
318 close_b = close_button()
320 form_widgets = []
322 def get_values():
323 return [pair[1].text().strip() for pair in form_widgets]
325 for name, value in inputs:
326 lineedit = QtWidgets.QLineEdit()
327 # Enable the OK button only when all fields have been populated
328 # pylint: disable=no-member
329 lineedit.textChanged.connect(
330 lambda x: ok_b.setEnabled(all(get_values())), type=Qt.QueuedConnection
332 if value:
333 lineedit.setText(value)
334 form_widgets.append((name, lineedit))
336 # layouts
337 form_layout = form(defs.no_margin, defs.button_spacing, *form_widgets)
338 button_layout = hbox(defs.no_margin, defs.button_spacing, STRETCH, close_b, ok_b)
339 main_layout = vbox(defs.margin, defs.button_spacing, form_layout, button_layout)
340 dialog.setLayout(main_layout)
342 # connections
343 connect_button(ok_b, dialog.accept)
344 connect_button(close_b, dialog.reject)
346 accepted = dialog.exec_() == QtWidgets.QDialog.Accepted
347 text = get_values()
348 success = accepted and all(text)
349 return (success, text)
352 def standard_item_type_value(value):
353 """Return a custom UserType for use in QTreeWidgetItem.type() overrides"""
354 return custom_item_type_value(QtGui.QStandardItem, value)
357 def graphics_item_type_value(value):
358 """Return a custom UserType for use in QGraphicsItem.type() overrides"""
359 return custom_item_type_value(QtWidgets.QGraphicsItem, value)
362 def custom_item_type_value(cls, value):
363 """Return a custom cls.UserType for use in cls.type() overrides"""
364 user_type = enum_value(cls.UserType)
365 return user_type + value
368 def enum_value(value):
369 """Qt6 has enums with an inner '.value' attribute."""
370 if hasattr(value, 'value'):
371 value = value.value
372 return value
375 class TreeWidgetItem(QtWidgets.QTreeWidgetItem):
377 TYPE = standard_item_type_value(101)
379 def __init__(self, path, icon, deleted):
380 QtWidgets.QTreeWidgetItem.__init__(self)
381 self.path = path
382 self.deleted = deleted
383 self.setIcon(0, icons.from_name(icon))
384 self.setText(0, path)
386 def type(self):
387 return self.TYPE
390 def paths_from_indexes(model, indexes, item_type=TreeWidgetItem.TYPE, item_filter=None):
391 """Return paths from a list of QStandardItemModel indexes"""
392 items = [model.itemFromIndex(i) for i in indexes]
393 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
396 def _true_filter(_value):
397 return True
400 def paths_from_items(items, item_type=TreeWidgetItem.TYPE, item_filter=None):
401 """Return a list of paths from a list of items"""
402 if item_filter is None:
403 item_filter = _true_filter
404 return [i.path for i in items if i.type() == item_type and item_filter(i)]
407 def tree_selection(tree_item, items):
408 """Returns an array of model items that correspond to the selected
409 QTreeWidgetItem children"""
410 selected = []
411 count = min(tree_item.childCount(), len(items))
412 for idx in range(count):
413 if tree_item.child(idx).isSelected():
414 selected.append(items[idx])
416 return selected
419 def tree_selection_items(tree_item):
420 """Returns selected widget items"""
421 selected = []
422 for idx in range(tree_item.childCount()):
423 child = tree_item.child(idx)
424 if child.isSelected():
425 selected.append(child)
427 return selected
430 def selected_item(list_widget, items):
431 """Returns the model item that corresponds to the selected QListWidget
432 row."""
433 widget_items = list_widget.selectedItems()
434 if not widget_items:
435 return None
436 widget_item = widget_items[0]
437 row = list_widget.row(widget_item)
438 if row < len(items):
439 item = items[row]
440 else:
441 item = None
442 return item
445 def selected_items(list_widget, items):
446 """Returns an array of model items that correspond to the selected
447 QListWidget rows."""
448 item_count = len(items)
449 selected = []
450 for widget_item in list_widget.selectedItems():
451 row = list_widget.row(widget_item)
452 if row < item_count:
453 selected.append(items[row])
454 return selected
457 def open_file(title, directory=None):
458 """Creates an Open File dialog and returns a filename."""
459 result = compat.getopenfilename(
460 parent=active_window(), caption=title, basedir=directory
462 return result[0]
465 def open_files(title, directory=None, filters=''):
466 """Creates an Open File dialog and returns a list of filenames."""
467 result = compat.getopenfilenames(
468 parent=active_window(), caption=title, basedir=directory, filters=filters
470 return result[0]
473 def opendir_dialog(caption, path):
474 """Prompts for a directory path"""
475 options = (
476 QtWidgets.QFileDialog.Directory
477 | QtWidgets.QFileDialog.DontResolveSymlinks
478 | QtWidgets.QFileDialog.ReadOnly
479 | QtWidgets.QFileDialog.ShowDirsOnly
481 return compat.getexistingdirectory(
482 parent=active_window(), caption=caption, basedir=path, options=options
486 def save_as(filename, title='Save As...'):
487 """Creates a Save File dialog and returns a filename."""
488 result = compat.getsavefilename(
489 parent=active_window(), caption=title, basedir=filename
491 return result[0]
494 def copy_path(filename, absolute=True):
495 """Copy a filename to the clipboard"""
496 if filename is None:
497 return
498 if absolute:
499 filename = core.abspath(filename)
500 set_clipboard(filename)
503 def set_clipboard(text):
504 """Sets the copy/paste buffer to text."""
505 if not text:
506 return
507 clipboard = QtWidgets.QApplication.clipboard()
508 clipboard.setText(text, QtGui.QClipboard.Clipboard)
509 if not utils.is_darwin() and not utils.is_win32():
510 clipboard.setText(text, QtGui.QClipboard.Selection)
511 persist_clipboard()
514 # pylint: disable=line-too-long
515 def persist_clipboard():
516 """Persist the clipboard
518 X11 stores only a reference to the clipboard data.
519 Send a clipboard event to force a copy of the clipboard to occur.
520 This ensures that the clipboard is present after git-cola exits.
521 Otherwise, the reference is destroyed on exit.
523 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
525 """ # noqa
526 clipboard = QtWidgets.QApplication.clipboard()
527 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
528 QtWidgets.QApplication.sendEvent(clipboard, event)
531 def add_action_bool(widget, text, func, checked, *shortcuts):
532 tip = text
533 action = _add_action(widget, text, tip, func, connect_action_bool, *shortcuts)
534 action.setCheckable(True)
535 action.setChecked(checked)
536 return action
539 def add_action(widget, text, func, *shortcuts):
540 """Create a QAction and bind it to the `func` callback and hotkeys"""
541 tip = text
542 return _add_action(widget, text, tip, func, connect_action, *shortcuts)
545 def add_action_with_icon(widget, icon, text, func, *shortcuts):
546 """Create a QAction using a custom icon bound to the `func` callback and hotkeys"""
547 tip = text
548 action = _add_action(widget, text, tip, func, connect_action, *shortcuts)
549 action.setIcon(icon)
550 return action
553 def add_action_with_status_tip(widget, text, tip, func, *shortcuts):
554 return _add_action(widget, text, tip, func, connect_action, *shortcuts)
557 def menu_separator(widget):
558 """Return a QAction whose isSeparator() returns true. Used in context menus"""
559 action = QtWidgets.QAction('', widget)
560 action.setSeparator(True)
561 return action
564 def _add_action(widget, text, tip, func, connect, *shortcuts):
565 action = QtWidgets.QAction(text, widget)
566 if hasattr(action, 'setIconVisibleInMenu'):
567 action.setIconVisibleInMenu(True)
568 if tip:
569 action.setStatusTip(tip)
570 connect(action, func)
571 if shortcuts:
572 action.setShortcuts(shortcuts)
573 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
574 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
575 widget.addAction(action)
576 return action
579 def set_selected_item(widget, idx):
580 """Sets a the currently selected item to the item at index idx."""
581 if isinstance(widget, QtWidgets.QTreeWidget):
582 item = widget.topLevelItem(idx)
583 if item:
584 item.setSelected(True)
585 widget.setCurrentItem(item)
588 def add_items(widget, items):
589 """Adds items to a widget."""
590 for item in items:
591 if item is None:
592 continue
593 widget.addItem(item)
596 def set_items(widget, items):
597 """Clear the existing widget contents and set the new items."""
598 widget.clear()
599 add_items(widget, items)
602 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
603 """Given a filename, return a TreeWidgetItem for a status widget
605 "staged", "deleted, and "untracked" control which icon is used.
608 icon_name = icons.status(filename, deleted, staged, untracked)
609 icon = icons.name_from_basename(icon_name)
610 return TreeWidgetItem(filename, icon, deleted=deleted)
613 def add_close_action(widget):
614 """Adds close action and shortcuts to a widget."""
615 return add_action(widget, N_('Close...'), widget.close, hotkeys.CLOSE, hotkeys.QUIT)
618 def app():
619 """Return the current application"""
620 return QtWidgets.QApplication.instance()
623 def desktop():
624 """Return the desktop"""
625 return app().desktop()
628 def desktop_size():
629 desk = desktop()
630 rect = desk.screenGeometry(QtGui.QCursor().pos())
631 return (rect.width(), rect.height())
634 def center_on_screen(widget):
635 """Move widget to the center of the default screen"""
636 width, height = desktop_size()
637 center_x = width // 2
638 center_y = height // 2
639 widget.move(center_x - widget.width() // 2, center_y - widget.height() // 2)
642 def default_size(parent, width, height, use_parent_height=True):
643 """Return the parent's size, or the provided defaults"""
644 if parent is not None:
645 width = parent.width()
646 if use_parent_height:
647 height = parent.height()
648 return (width, height)
651 def default_monospace_font():
652 if utils.is_darwin():
653 family = 'Monaco'
654 else:
655 family = 'Monospace'
656 mfont = QtGui.QFont()
657 mfont.setFamily(family)
658 return mfont
661 def diff_font_str(context):
662 cfg = context.cfg
663 font_str = cfg.get(prefs.FONTDIFF)
664 if not font_str:
665 font_str = default_monospace_font().toString()
666 return font_str
669 def diff_font(context):
670 return font(diff_font_str(context))
673 def font(string):
674 qfont = QtGui.QFont()
675 qfont.fromString(string)
676 return qfont
679 def create_button(
680 text='', layout=None, tooltip=None, icon=None, enabled=True, default=False
682 """Create a button, set its title, and add it to the parent."""
683 button = QtWidgets.QPushButton()
684 button.setCursor(Qt.PointingHandCursor)
685 button.setFocusPolicy(Qt.NoFocus)
686 if text:
687 button.setText(' ' + text)
688 if icon is not None:
689 button.setIcon(icon)
690 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
691 if tooltip is not None:
692 button.setToolTip(tooltip)
693 if layout is not None:
694 layout.addWidget(button)
695 if not enabled:
696 button.setEnabled(False)
697 if default:
698 button.setDefault(True)
699 return button
702 def tool_button():
703 """Create a flat border-less button"""
704 button = QtWidgets.QToolButton()
705 button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
706 button.setCursor(Qt.PointingHandCursor)
707 button.setFocusPolicy(Qt.NoFocus)
708 # Highlight colors
709 palette = QtGui.QPalette()
710 highlight = palette.color(QtGui.QPalette.Highlight)
711 highlight_rgb = rgb_css(highlight)
713 button.setStyleSheet(
715 /* No borders */
716 QToolButton {
717 border: none;
718 background-color: none;
720 /* Hide the menu indicator */
721 QToolButton::menu-indicator {
722 image: none;
724 QToolButton:hover {
725 border: %(border)spx solid %(highlight_rgb)s;
728 % dict(border=defs.border, highlight_rgb=highlight_rgb)
730 return button
733 def create_action_button(tooltip=None, icon=None, visible=True):
734 """Create a small toolbutton for use in dock title widgets"""
735 button = tool_button()
736 if tooltip is not None:
737 button.setToolTip(tooltip)
738 if icon is not None:
739 button.setIcon(icon)
740 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
741 button.setVisible(visible)
742 return button
745 def ok_button(text, default=True, enabled=True, icon=None):
746 if icon is None:
747 icon = icons.ok()
748 return create_button(text=text, icon=icon, default=default, enabled=enabled)
751 def close_button(text=None, icon=None):
752 text = text or N_('Close')
753 icon = icons.mkicon(icon, icons.close)
754 return create_button(text=text, icon=icon)
757 def edit_button(enabled=True, default=False):
758 return create_button(
759 text=N_('Edit'), icon=icons.edit(), enabled=enabled, default=default
763 def refresh_button(enabled=True, default=False):
764 return create_button(
765 text=N_('Refresh'), icon=icons.sync(), enabled=enabled, default=default
769 def checkbox(text='', tooltip='', checked=None):
770 """Create a checkbox"""
771 return _checkbox(QtWidgets.QCheckBox, text, tooltip, checked)
774 def radio(text='', tooltip='', checked=None):
775 """Create a radio button"""
776 return _checkbox(QtWidgets.QRadioButton, text, tooltip, checked)
779 def _checkbox(cls, text, tooltip, checked):
780 """Create a widget and apply properties"""
781 widget = cls()
782 if text:
783 widget.setText(text)
784 if tooltip:
785 widget.setToolTip(tooltip)
786 if checked is not None:
787 widget.setChecked(checked)
788 return widget
791 class DockTitleBarWidget(QtWidgets.QFrame):
792 def __init__(self, parent, title, stretch=True):
793 QtWidgets.QFrame.__init__(self, parent)
794 self.setAutoFillBackground(True)
795 self.label = qlabel = QtWidgets.QLabel(title, self)
796 qfont = qlabel.font()
797 qfont.setBold(True)
798 qlabel.setFont(qfont)
799 qlabel.setCursor(Qt.OpenHandCursor)
801 self.close_button = create_action_button(
802 tooltip=N_('Close'), icon=icons.close()
805 self.toggle_button = create_action_button(
806 tooltip=N_('Detach'), icon=icons.external()
809 self.corner_layout = hbox(defs.no_margin, defs.spacing)
811 if stretch:
812 separator = STRETCH
813 else:
814 separator = SKIPPED
816 self.main_layout = hbox(
817 defs.small_margin,
818 defs.titlebar_spacing,
819 qlabel,
820 separator,
821 self.corner_layout,
822 self.toggle_button,
823 self.close_button,
825 self.setLayout(self.main_layout)
827 connect_button(self.toggle_button, self.toggle_floating)
828 connect_button(self.close_button, self.toggle_visibility)
830 def toggle_floating(self):
831 self.parent().setFloating(not self.parent().isFloating())
832 self.update_tooltips()
834 def toggle_visibility(self):
835 self.parent().toggleViewAction().trigger()
837 def set_title(self, title):
838 self.label.setText(title)
840 def add_corner_widget(self, widget):
841 self.corner_layout.addWidget(widget)
843 def update_tooltips(self):
844 if self.parent().isFloating():
845 tooltip = N_('Attach')
846 else:
847 tooltip = N_('Detach')
848 self.toggle_button.setToolTip(tooltip)
851 def create_dock(name, title, parent, stretch=True, widget=None, func=None):
852 """Create a dock widget and set it up accordingly."""
853 dock = QtWidgets.QDockWidget(parent)
854 dock.setWindowTitle(title)
855 dock.setObjectName(name)
856 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
857 dock.setTitleBarWidget(titlebar)
858 dock.setAutoFillBackground(True)
859 if hasattr(parent, 'dockwidgets'):
860 parent.dockwidgets.append(dock)
861 if func:
862 widget = func(dock)
863 if widget:
864 dock.setWidget(widget)
865 return dock
868 def hide_dock(widget):
869 widget.toggleViewAction().setChecked(False)
870 widget.hide()
873 def create_menu(title, parent):
874 """Create a menu and set its title."""
875 qmenu = DebouncingMenu(title, parent)
876 return qmenu
879 class DebouncingMenu(QtWidgets.QMenu):
880 """Menu that debounces mouse release action ie. stops it if occurred
881 right after menu creation.
883 Disables annoying behaviour when RMB is pressed to show menu, cursor is
884 moved accidentally 1px onto newly created menu and released causing to
885 execute menu action
888 threshold_ms = 400
890 def __init__(self, title, parent):
891 QtWidgets.QMenu.__init__(self, title, parent)
892 self.created_at = utils.epoch_millis()
893 if hasattr(self, 'setToolTipsVisible'):
894 self.setToolTipsVisible(True)
896 def mouseReleaseEvent(self, event):
897 threshold = DebouncingMenu.threshold_ms
898 if (utils.epoch_millis() - self.created_at) > threshold:
899 QtWidgets.QMenu.mouseReleaseEvent(self, event)
902 def add_menu(title, parent):
903 """Create a menu and set its title."""
904 menu = create_menu(title, parent)
905 if hasattr(parent, 'addMenu'):
906 parent.addMenu(menu)
907 else:
908 parent.addAction(menu.menuAction())
909 return menu
912 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
913 button = tool_button()
914 if icon is not None:
915 button.setIcon(icon)
916 button.setIconSize(QtCore.QSize(defs.default_icon, defs.default_icon))
917 if text is not None:
918 button.setText(' ' + text)
919 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
920 if tooltip is not None:
921 button.setToolTip(tooltip)
922 if layout is not None:
923 layout.addWidget(button)
924 return button
927 def create_toolbutton_with_callback(callback, text, icon, tooltip, layout=None):
928 """Create a toolbutton that runs the specified callback"""
929 toolbutton = create_toolbutton(text=text, layout=layout, tooltip=tooltip, icon=icon)
930 connect_button(toolbutton, callback)
931 return toolbutton
934 # pylint: disable=line-too-long
935 def mimedata_from_paths(context, paths, include_urls=True):
936 """Return mimedata with a list of absolute path URLs
938 Set `include_urls` to False to prevent URLs from being included
939 in the mimedata. This is useful in some terminals that do not gracefully handle
940 multiple URLs being included in the payload.
942 This allows the mimedata to contain just plain a plain text value that we
943 are able to format ourselves.
945 Older verisons of gnome-terminal expected a utf-16 encoding, but that
946 behavior is no longer needed.
947 """ # noqa
948 abspaths = [core.abspath(path) for path in paths]
949 paths_text = core.list2cmdline(abspaths)
951 # The text/x-moz-list format is always included by Qt, and doing
952 # mimedata.removeFormat('text/x-moz-url') has no effect.
953 # http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
955 # Older versions of gnome-terminal expect utf-16 encoded text, but other terminals,
956 # e.g. terminator, expect utf-8, so use cola.dragencoding to override the default.
957 # NOTE: text/x-moz-url does not seem to be used/needed by modern versions of
958 # gnome-terminal, kitty, and terminator.
959 mimedata = QtCore.QMimeData()
960 mimedata.setText(paths_text)
961 if include_urls:
962 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
963 encoding = context.cfg.get('cola.dragencoding', 'utf-16')
964 encoded_text = core.encode(paths_text, encoding=encoding)
965 mimedata.setUrls(urls)
966 mimedata.setData('text/x-moz-url', encoded_text)
968 return mimedata
971 def path_mimetypes(include_urls=True):
972 """Return a list of mimetypes that we generate"""
973 mime_types = [
974 'text/plain',
975 'text/plain;charset=utf-8',
977 if include_urls:
978 mime_types.append('text/uri-list')
979 mime_types.append('text/x-moz-url')
980 return mime_types
983 class BlockSignals(object):
984 """Context manager for blocking a signals on a widget"""
986 def __init__(self, *widgets):
987 self.widgets = widgets
988 self.values = []
990 def __enter__(self):
991 """Block Qt signals for all of the captured widgets"""
992 self.values = [widget.blockSignals(True) for widget in self.widgets]
993 return self
995 def __exit__(self, exc_type, exc_val, exc_tb):
996 """Restore Qt signals when we exit the scope"""
997 for (widget, value) in zip(self.widgets, self.values):
998 widget.blockSignals(value)
1001 class Channel(QtCore.QObject):
1002 finished = Signal(object)
1003 result = Signal(object)
1006 class Task(QtCore.QRunnable):
1007 """Run a task in the background and return the result using a Channel"""
1009 def __init__(self):
1010 QtCore.QRunnable.__init__(self)
1012 self.channel = Channel()
1013 self.result = None
1014 # Python's garbage collector will try to double-free the task
1015 # once it's finished, so disable Qt's auto-deletion as a workaround.
1016 self.setAutoDelete(False)
1018 def run(self):
1019 self.result = self.task()
1020 self.channel.result.emit(self.result)
1021 self.channel.finished.emit(self)
1023 def task(self):
1024 """Perform a long-running task"""
1025 return ()
1027 def connect(self, handler):
1028 self.channel.result.connect(handler, type=Qt.QueuedConnection)
1031 class SimpleTask(Task):
1032 """Run a simple callable as a task"""
1034 def __init__(self, func, *args, **kwargs):
1035 Task.__init__(self)
1037 self.func = func
1038 self.args = args
1039 self.kwargs = kwargs
1041 def task(self):
1042 return self.func(*self.args, **self.kwargs)
1045 class RunTask(QtCore.QObject):
1046 """Runs QRunnable instances and transfers control when they finish"""
1048 def __init__(self, parent=None):
1049 QtCore.QObject.__init__(self, parent)
1050 self.tasks = []
1051 self.task_details = {}
1052 self.threadpool = QtCore.QThreadPool.globalInstance()
1053 self.result_func = None
1055 def start(self, task, progress=None, finish=None, result=None):
1056 """Start the task and register a callback"""
1057 self.result_func = result
1058 if progress is not None:
1059 progress.show()
1060 if hasattr(progress, 'start'):
1061 progress.start()
1063 # prevents garbage collection bugs in certain PyQt4 versions
1064 self.tasks.append(task)
1065 task_id = id(task)
1066 self.task_details[task_id] = (progress, finish, result)
1067 task.channel.finished.connect(self.finish, type=Qt.QueuedConnection)
1068 self.threadpool.start(task)
1070 def finish(self, task):
1071 """The task has finished. Run the finish and result callbacks"""
1072 task_id = id(task)
1073 try:
1074 self.tasks.remove(task)
1075 except ValueError:
1076 pass
1077 try:
1078 progress, finish, result = self.task_details[task_id]
1079 del self.task_details[task_id]
1080 except KeyError:
1081 finish = progress = result = None
1083 if progress is not None:
1084 if hasattr(progress, 'stop'):
1085 progress.stop()
1086 progress.hide()
1088 if result is not None:
1089 result(task.result)
1091 if finish is not None:
1092 finish(task)
1095 # Syntax highlighting
1098 def rgb(red, green, blue):
1099 """Create a QColor from r, g, b arguments"""
1100 color = QtGui.QColor()
1101 color.setRgb(red, green, blue)
1102 return color
1105 def rgba(red, green, blue, alpha=255):
1106 """Create a QColor with alpha from r, g, b, a arguments"""
1107 color = rgb(red, green, blue)
1108 color.setAlpha(alpha)
1109 return color
1112 def rgb_triple(args):
1113 """Create a QColor from an argument with an [r, g, b] triple"""
1114 return rgb(*args)
1117 def rgb_css(color):
1118 """Convert a QColor into an rgb #abcdef CSS string"""
1119 return '#%s' % rgb_hex(color)
1122 def rgb_hex(color):
1123 """Convert a QColor into a hex aabbcc string"""
1124 return '%02x%02x%02x' % (color.red(), color.green(), color.blue())
1127 def clamp_color(value):
1128 """Clamp an integer value between 0 and 255"""
1129 return min(255, max(value, 0))
1132 def css_color(value):
1133 """Convert a #abcdef hex string into a QColor"""
1134 if value.startswith('#'):
1135 value = value[1:]
1136 try:
1137 red = clamp_color(int(value[:2], base=16)) # ab
1138 except ValueError:
1139 red = 255
1140 try:
1141 green = clamp_color(int(value[2:4], base=16)) # cd
1142 except ValueError:
1143 green = 255
1144 try:
1145 blue = clamp_color(int(value[4:6], base=16)) # ef
1146 except ValueError:
1147 blue = 255
1148 return rgb(red, green, blue)
1151 def hsl(hue, saturation, lightness):
1152 """Return a QColor from an hue, saturation and lightness"""
1153 return QtGui.QColor.fromHslF(
1154 utils.clamp(hue, 0.0, 1.0),
1155 utils.clamp(saturation, 0.0, 1.0),
1156 utils.clamp(lightness, 0.0, 1.0),
1160 def hsl_css(hue, saturation, lightness):
1161 """Convert HSL values to a CSS #abcdef color string"""
1162 return rgb_css(hsl(hue, saturation, lightness))
1165 def make_format(foreground=None, background=None, bold=False):
1166 """Create a QTextFormat from the provided foreground, background and bold values"""
1167 fmt = QtGui.QTextCharFormat()
1168 if foreground:
1169 fmt.setForeground(foreground)
1170 if background:
1171 fmt.setBackground(background)
1172 if bold:
1173 fmt.setFontWeight(QtGui.QFont.Bold)
1174 return fmt
1177 class ImageFormats(object):
1178 def __init__(self):
1179 # returns a list of QByteArray objects
1180 formats_qba = QtGui.QImageReader.supportedImageFormats()
1181 # portability: python3 data() returns bytes, python2 returns str
1182 decode = core.decode
1183 formats = [decode(x.data()) for x in formats_qba]
1184 self.extensions = {'.' + fmt for fmt in formats}
1186 def ok(self, filename):
1187 _, ext = os.path.splitext(filename)
1188 return ext.lower() in self.extensions
1191 def set_scrollbar_values(widget, hscroll_value, vscroll_value):
1192 """Set scrollbars to the specified values"""
1193 hscroll = widget.horizontalScrollBar()
1194 if hscroll and hscroll_value is not None:
1195 hscroll.setValue(hscroll_value)
1197 vscroll = widget.verticalScrollBar()
1198 if vscroll and vscroll_value is not None:
1199 vscroll.setValue(vscroll_value)
1202 def get_scrollbar_values(widget):
1203 """Return the current (hscroll, vscroll) scrollbar values for a widget"""
1204 hscroll = widget.horizontalScrollBar()
1205 if hscroll:
1206 hscroll_value = get(hscroll)
1207 else:
1208 hscroll_value = None
1209 vscroll = widget.verticalScrollBar()
1210 if vscroll:
1211 vscroll_value = get(vscroll)
1212 else:
1213 vscroll_value = None
1214 return (hscroll_value, vscroll_value)
1217 def scroll_to_item(widget, item):
1218 """Scroll to an item while retaining the horizontal scroll position"""
1219 hscroll = None
1220 hscrollbar = widget.horizontalScrollBar()
1221 if hscrollbar:
1222 hscroll = get(hscrollbar)
1223 widget.scrollToItem(item)
1224 if hscroll is not None:
1225 hscrollbar.setValue(hscroll)
1228 def select_item(widget, item):
1229 """Scroll to and make a QTreeWidget item selected and current"""
1230 scroll_to_item(widget, item)
1231 widget.setCurrentItem(item)
1232 item.setSelected(True)
1235 def get_selected_values(widget, top_level_idx, values):
1236 """Map the selected items under the top-level item to the values list"""
1237 # Get the top-level item
1238 item = widget.topLevelItem(top_level_idx)
1239 return tree_selection(item, values)
1242 def get_selected_items(widget, idx):
1243 """Return the selected items under the top-level item"""
1244 item = widget.topLevelItem(idx)
1245 return tree_selection_items(item)
1248 def add_menu_actions(menu, menu_actions):
1249 """Add actions to a menu, treating None as a separator"""
1250 current_actions = menu.actions()
1251 if current_actions:
1252 first_action = current_actions[0]
1253 else:
1254 first_action = None
1255 menu.addSeparator()
1257 for action in menu_actions:
1258 if action is None:
1259 action = menu_separator(menu)
1260 menu.insertAction(first_action, action)