1 """Miscellaneous Qt utility functions."""
2 from __future__
import absolute_import
, division
, print_function
, unicode_literals
5 from qtpy
import compat
7 from qtpy
import QtCore
8 from qtpy
import QtWidgets
9 from qtpy
.QtCore
import Qt
10 from qtpy
.QtCore
import Signal
17 from .compat
import int_types
18 from .compat
import ustr
19 from .models
import prefs
20 from .widgets
import defs
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(), type=Qt
.QueuedConnection
)
37 def connect_action_bool(action
, fn
):
38 """Connect a triggered(bool) action to a function"""
39 action
.triggered
[bool].connect(fn
, type=Qt
.QueuedConnection
)
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(), type=Qt
.QueuedConnection
)
49 def connect_checkbox(widget
, fn
):
50 """Connect a checkbox to a function taking bool"""
51 widget
.clicked
.connect(
52 lambda *args
, **kwargs
: fn(get(checkbox
)), type=Qt
.QueuedConnection
56 def connect_released(button
, fn
):
57 """Connect a button to a function"""
58 button
.released
.connect(fn
, type=Qt
.QueuedConnection
)
61 def button_action(button
, action
):
62 """Make a button trigger an action"""
63 connect_button(button
, action
.trigger
)
66 def connect_toggle(toggle
, fn
):
67 """Connect a toggle button to a function"""
68 toggle
.toggled
.connect(fn
, type=Qt
.QueuedConnection
)
71 def disconnect(signal
):
72 """Disconnect signal from all slots"""
75 except TypeError: # allow unconnected slots
80 """Query a widget for its python value"""
81 if hasattr(widget
, 'isChecked'):
82 value
= widget
.isChecked()
83 elif hasattr(widget
, 'value'):
84 value
= widget
.value()
85 elif hasattr(widget
, 'text'):
87 elif hasattr(widget
, 'toPlainText'):
88 value
= widget
.toPlainText()
89 elif hasattr(widget
, 'sizes'):
90 value
= widget
.sizes()
91 elif hasattr(widget
, 'date'):
92 value
= widget
.date().toString(Qt
.ISODate
)
98 def hbox(margin
, spacing
, *items
):
99 """Create an HBoxLayout with the specified sizes and items"""
100 return box(QtWidgets
.QHBoxLayout
, margin
, spacing
, *items
)
103 def vbox(margin
, spacing
, *items
):
104 """Create a VBoxLayout with the specified sizes and items"""
105 return box(QtWidgets
.QVBoxLayout
, margin
, spacing
, *items
)
108 def buttongroup(*items
):
109 """Create a QButtonGroup for the specified items"""
110 group
= QtWidgets
.QButtonGroup()
116 def set_margin(layout
, margin
):
117 """Set the content margins for a layout"""
118 layout
.setContentsMargins(margin
, margin
, margin
, margin
)
121 def box(cls
, margin
, spacing
, *items
):
122 """Create a QBoxLayout with the specified sizes and items"""
126 layout
.setSpacing(spacing
)
127 set_margin(layout
, margin
)
130 if isinstance(i
, QtWidgets
.QWidget
):
135 QtWidgets
.QHBoxLayout
,
136 QtWidgets
.QVBoxLayout
,
137 QtWidgets
.QFormLayout
,
146 elif isinstance(i
, int_types
):
152 def form(margin
, spacing
, *widgets
):
153 """Create a QFormLayout with the specified sizes and items"""
154 layout
= QtWidgets
.QFormLayout()
155 layout
.setSpacing(spacing
)
156 layout
.setFieldGrowthPolicy(QtWidgets
.QFormLayout
.ExpandingFieldsGrow
)
157 set_margin(layout
, margin
)
159 for idx
, (name
, widget
) in enumerate(widgets
):
160 if isinstance(name
, (str, ustr
)):
161 layout
.addRow(name
, widget
)
163 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.LabelRole
, name
)
164 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.FieldRole
, widget
)
169 def grid(margin
, spacing
, *widgets
):
170 """Create a QGridLayout with the specified sizes and items"""
171 layout
= QtWidgets
.QGridLayout()
172 layout
.setSpacing(spacing
)
173 set_margin(layout
, margin
)
177 if isinstance(item
, QtWidgets
.QWidget
):
178 layout
.addWidget(*row
)
179 elif isinstance(item
, QtWidgets
.QLayoutItem
):
185 def splitter(orientation
, *widgets
):
186 """Create a spliter over the specified widgets
188 :param orientation: Qt.Horizontal or Qt.Vertical
191 layout
= QtWidgets
.QSplitter()
192 layout
.setOrientation(orientation
)
193 layout
.setHandleWidth(defs
.handle_width
)
194 layout
.setChildrenCollapsible(True)
196 for idx
, widget
in enumerate(widgets
):
197 layout
.addWidget(widget
)
198 layout
.setStretchFactor(idx
, 1)
200 # Workaround for Qt not setting the WA_Hover property for QSplitter
201 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
202 layout
.handle(1).setAttribute(Qt
.WA_Hover
)
207 def label(text
=None, align
=None, fmt
=None, selectable
=True):
208 """Create a QLabel with the specified properties"""
209 widget
= QtWidgets
.QLabel()
210 if align
is not None:
211 widget
.setAlignment(align
)
213 widget
.setTextFormat(fmt
)
215 widget
.setTextInteractionFlags(Qt
.TextBrowserInteraction
)
216 widget
.setOpenExternalLinks(True)
222 class ComboBox(QtWidgets
.QComboBox
):
223 """Custom read-only combobox with a convenient API"""
225 def __init__(self
, items
=None, editable
=False, parent
=None, transform
=None):
226 super(ComboBox
, self
).__init
__(parent
)
227 self
.setEditable(editable
)
228 self
.transform
= transform
232 self
.item_data
.extend(items
)
234 def set_index(self
, idx
):
235 idx
= utils
.clamp(idx
, 0, self
.count() - 1)
236 self
.setCurrentIndex(idx
)
238 def add_item(self
, text
, data
):
240 self
.item_data
.append(data
)
242 def current_data(self
):
243 return self
.item_data
[self
.currentIndex()]
245 def set_value(self
, value
):
247 value
= self
.transform(value
)
249 index
= self
.item_data
.index(value
)
252 self
.setCurrentIndex(index
)
255 def combo(items
, editable
=False, parent
=None):
256 """Create a readonly (by default) combobox from a list of items"""
257 return ComboBox(editable
=editable
, items
=items
, parent
=parent
)
260 def combo_mapped(data
, editable
=False, transform
=None, parent
=None):
261 """Create a readonly (by default) combobox from a list of items"""
262 widget
= ComboBox(editable
=editable
, transform
=transform
, parent
=parent
)
264 widget
.add_item(k
, v
)
268 def textbrowser(text
=None):
269 """Create a QTextBrowser for the specified text"""
270 widget
= QtWidgets
.QTextBrowser()
271 widget
.setOpenExternalLinks(True)
277 def add_completer(widget
, items
):
278 """Add simple completion to a widget"""
279 completer
= QtWidgets
.QCompleter(items
, widget
)
280 completer
.setCaseSensitivity(Qt
.CaseInsensitive
)
281 completer
.setCompletionMode(QtWidgets
.QCompleter
.InlineCompletion
)
282 widget
.setCompleter(completer
)
285 def prompt(msg
, title
=None, text
='', parent
=None):
286 """Presents the user with an input widget and returns the input."""
290 parent
= active_window()
291 result
= QtWidgets
.QInputDialog
.getText(
292 parent
, title
, msg
, QtWidgets
.QLineEdit
.Normal
, text
294 return (result
[0], result
[1])
297 def prompt_n(msg
, inputs
):
298 """Presents the user with N input widgets and returns the results"""
299 dialog
= QtWidgets
.QDialog(active_window())
300 dialog
.setWindowModality(Qt
.WindowModal
)
301 dialog
.setWindowTitle(msg
)
305 if len(k
+ v
) > len(long_value
):
308 metrics
= QtGui
.QFontMetrics(dialog
.font())
309 min_width
= min(720, metrics
.width(long_value
) + 100)
310 dialog
.setMinimumWidth(min_width
)
312 ok_b
= ok_button(msg
, enabled
=False)
313 close_b
= close_button()
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(
325 lambda x
: ok_b
.setEnabled(all(get_values())), type=Qt
.QueuedConnection
328 lineedit
.setText(value
)
329 form_widgets
.append((name
, lineedit
))
332 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
333 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, STRETCH
, close_b
, ok_b
)
334 main_layout
= vbox(defs
.margin
, defs
.button_spacing
, form_layout
, button_layout
)
335 dialog
.setLayout(main_layout
)
338 connect_button(ok_b
, dialog
.accept
)
339 connect_button(close_b
, dialog
.reject
)
341 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
343 ok
= accepted
and all(text
)
347 def standard_item_type_value(value
):
348 """Return a custom UserType for use in QTreeWidgetItem.type() overrides"""
349 return custom_item_type_value(QtGui
.QStandardItem
, value
)
352 def graphics_item_type_value(value
):
353 """Return a custom UserType for use in QGraphicsItem.type() overrides"""
354 return custom_item_type_value(QtWidgets
.QGraphicsItem
, value
)
357 def custom_item_type_value(cls
, value
):
358 """Return a custom cls.UserType for use in cls.type() overrides"""
359 user_type
= enum_value(cls
.UserType
)
360 return user_type
+ value
363 def enum_value(value
):
364 """Qt6 has enums with an inner '.value' attribute."""
365 if hasattr(value
, 'value'):
370 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
372 TYPE
= standard_item_type_value(101)
374 def __init__(self
, path
, icon
, deleted
):
375 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
377 self
.deleted
= deleted
378 self
.setIcon(0, icons
.from_name(icon
))
379 self
.setText(0, path
)
385 def paths_from_indexes(model
, indexes
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
386 """Return paths from a list of QStandardItemModel indexes"""
387 items
= [model
.itemFromIndex(i
) for i
in indexes
]
388 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
391 def _true_filter(_x
):
395 def paths_from_items(items
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
396 """Return a list of paths from a list of items"""
397 if item_filter
is None:
398 item_filter
= _true_filter
399 return [i
.path
for i
in items
if i
.type() == item_type
and item_filter(i
)]
402 def tree_selection(tree_item
, items
):
403 """Returns an array of model items that correspond to the selected
404 QTreeWidgetItem children"""
406 count
= min(tree_item
.childCount(), len(items
))
407 for idx
in range(count
):
408 if tree_item
.child(idx
).isSelected():
409 selected
.append(items
[idx
])
414 def tree_selection_items(tree_item
):
415 """Returns selected widget items"""
417 for idx
in range(tree_item
.childCount()):
418 child
= tree_item
.child(idx
)
419 if child
.isSelected():
420 selected
.append(child
)
425 def selected_item(list_widget
, items
):
426 """Returns the model item that corresponds to the selected QListWidget
428 widget_items
= list_widget
.selectedItems()
431 widget_item
= widget_items
[0]
432 row
= list_widget
.row(widget_item
)
440 def selected_items(list_widget
, items
):
441 """Returns an array of model items that correspond to the selected
443 item_count
= len(items
)
445 for widget_item
in list_widget
.selectedItems():
446 row
= list_widget
.row(widget_item
)
448 selected
.append(items
[row
])
452 def open_file(title
, directory
=None):
453 """Creates an Open File dialog and returns a filename."""
454 result
= compat
.getopenfilename(
455 parent
=active_window(), caption
=title
, basedir
=directory
460 def open_files(title
, directory
=None, filters
=''):
461 """Creates an Open File dialog and returns a list of filenames."""
462 result
= compat
.getopenfilenames(
463 parent
=active_window(), caption
=title
, basedir
=directory
, filters
=filters
468 def opendir_dialog(caption
, path
):
469 """Prompts for a directory path"""
471 QtWidgets
.QFileDialog
.Directory
472 | QtWidgets
.QFileDialog
.DontResolveSymlinks
473 | QtWidgets
.QFileDialog
.ReadOnly
474 | QtWidgets
.QFileDialog
.ShowDirsOnly
476 return compat
.getexistingdirectory(
477 parent
=active_window(), caption
=caption
, basedir
=path
, options
=options
481 def save_as(filename
, title
='Save As...'):
482 """Creates a Save File dialog and returns a filename."""
483 result
= compat
.getsavefilename(
484 parent
=active_window(), caption
=title
, basedir
=filename
489 def copy_path(filename
, absolute
=True):
490 """Copy a filename to the clipboard"""
494 filename
= core
.abspath(filename
)
495 set_clipboard(filename
)
498 def set_clipboard(text
):
499 """Sets the copy/paste buffer to text."""
502 clipboard
= QtWidgets
.QApplication
.clipboard()
503 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
504 if not utils
.is_darwin() and not utils
.is_win32():
505 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
509 # pylint: disable=line-too-long
510 def persist_clipboard():
511 """Persist the clipboard
513 X11 stores only a reference to the clipboard data.
514 Send a clipboard event to force a copy of the clipboard to occur.
515 This ensures that the clipboard is present after git-cola exits.
516 Otherwise, the reference is destroyed on exit.
518 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
521 clipboard
= QtWidgets
.QApplication
.clipboard()
522 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
523 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
526 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
528 action
= _add_action(widget
, text
, tip
, fn
, connect_action_bool
, *shortcuts
)
529 action
.setCheckable(True)
530 action
.setChecked(checked
)
534 def add_action(widget
, text
, fn
, *shortcuts
):
536 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
539 def add_action_with_status_tip(widget
, text
, tip
, fn
, *shortcuts
):
540 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
543 def _add_action(widget
, text
, tip
, fn
, connect
, *shortcuts
):
544 action
= QtWidgets
.QAction(text
, widget
)
545 if hasattr(action
, 'setIconVisibleInMenu'):
546 action
.setIconVisibleInMenu(True)
548 action
.setStatusTip(tip
)
551 action
.setShortcuts(shortcuts
)
552 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
553 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
554 widget
.addAction(action
)
558 def set_selected_item(widget
, idx
):
559 """Sets a the currently selected item to the item at index idx."""
560 if isinstance(widget
, QtWidgets
.QTreeWidget
):
561 item
= widget
.topLevelItem(idx
)
563 item
.setSelected(True)
564 widget
.setCurrentItem(item
)
567 def add_items(widget
, items
):
568 """Adds items to a widget."""
575 def set_items(widget
, items
):
576 """Clear the existing widget contents and set the new items."""
578 add_items(widget
, items
)
581 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
582 """Given a filename, return a TreeWidgetItem for a status widget
584 "staged", "deleted, and "untracked" control which icon is used.
587 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
588 icon
= icons
.name_from_basename(icon_name
)
589 return TreeWidgetItem(filename
, icon
, deleted
=deleted
)
592 def add_close_action(widget
):
593 """Adds close action and shortcuts to a widget."""
594 return add_action(widget
, N_('Close...'), widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
598 """Return the current application"""
599 return QtWidgets
.QApplication
.instance()
603 """Return the desktop"""
604 return app().desktop()
609 rect
= desk
.screenGeometry(QtGui
.QCursor().pos())
610 return (rect
.width(), rect
.height())
613 def center_on_screen(widget
):
614 """Move widget to the center of the default screen"""
615 width
, height
= desktop_size()
618 widget
.move(cx
- widget
.width() // 2, cy
- widget
.height() // 2)
621 def default_size(parent
, width
, height
, use_parent_height
=True):
622 """Return the parent's size, or the provided defaults"""
623 if parent
is not None:
624 width
= parent
.width()
625 if use_parent_height
:
626 height
= parent
.height()
627 return (width
, height
)
630 def default_monospace_font():
631 if utils
.is_darwin():
635 mfont
= QtGui
.QFont()
636 mfont
.setFamily(family
)
640 def diff_font_str(context
):
642 font_str
= cfg
.get(prefs
.FONTDIFF
)
644 font_str
= default_monospace_font().toString()
648 def diff_font(context
):
649 return font(diff_font_str(context
))
653 qfont
= QtGui
.QFont()
654 qfont
.fromString(string
)
659 text
='', layout
=None, tooltip
=None, icon
=None, enabled
=True, default
=False
661 """Create a button, set its title, and add it to the parent."""
662 button
= QtWidgets
.QPushButton()
663 button
.setCursor(Qt
.PointingHandCursor
)
664 button
.setFocusPolicy(Qt
.NoFocus
)
666 button
.setText(' ' + text
)
669 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
670 if tooltip
is not None:
671 button
.setToolTip(tooltip
)
672 if layout
is not None:
673 layout
.addWidget(button
)
675 button
.setEnabled(False)
677 button
.setDefault(True)
682 """Create a flat border-less button"""
683 button
= QtWidgets
.QToolButton()
684 button
.setPopupMode(QtWidgets
.QToolButton
.InstantPopup
)
685 button
.setCursor(Qt
.PointingHandCursor
)
686 button
.setFocusPolicy(Qt
.NoFocus
)
688 palette
= QtGui
.QPalette()
689 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
690 highlight_rgb
= rgb_css(highlight
)
692 button
.setStyleSheet(
697 background-color: none;
699 /* Hide the menu indicator */
700 QToolButton::menu-indicator {
704 border: %(border)spx solid %(highlight_rgb)s;
707 % dict(border
=defs
.border
, highlight_rgb
=highlight_rgb
)
712 def create_action_button(tooltip
=None, icon
=None, visible
=True):
713 """Create a small toolbutton for use in dock title widgets"""
714 button
= tool_button()
715 if tooltip
is not None:
716 button
.setToolTip(tooltip
)
719 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
720 button
.setVisible(visible
)
724 def ok_button(text
, default
=True, enabled
=True, icon
=None):
727 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
730 def close_button(text
=None, icon
=None):
731 text
= text
or N_('Close')
732 icon
= icons
.mkicon(icon
, icons
.close
)
733 return create_button(text
=text
, icon
=icon
)
736 def edit_button(enabled
=True, default
=False):
737 return create_button(
738 text
=N_('Edit'), icon
=icons
.edit(), enabled
=enabled
, default
=default
742 def refresh_button(enabled
=True, default
=False):
743 return create_button(
744 text
=N_('Refresh'), icon
=icons
.sync(), enabled
=enabled
, default
=default
748 def checkbox(text
='', tooltip
='', checked
=None):
749 """Create a checkbox"""
750 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
753 def radio(text
='', tooltip
='', checked
=None):
754 """Create a radio button"""
755 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
758 def _checkbox(cls
, text
, tooltip
, checked
):
759 """Create a widget and apply properties"""
764 widget
.setToolTip(tooltip
)
765 if checked
is not None:
766 widget
.setChecked(checked
)
770 class DockTitleBarWidget(QtWidgets
.QFrame
):
771 def __init__(self
, parent
, title
, stretch
=True):
772 QtWidgets
.QFrame
.__init
__(self
, parent
)
773 self
.setAutoFillBackground(True)
774 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
775 qfont
= qlabel
.font()
777 qlabel
.setFont(qfont
)
778 qlabel
.setCursor(Qt
.OpenHandCursor
)
780 self
.close_button
= create_action_button(
781 tooltip
=N_('Close'), icon
=icons
.close()
784 self
.toggle_button
= create_action_button(
785 tooltip
=N_('Detach'), icon
=icons
.external()
788 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
795 self
.main_layout
= hbox(
797 defs
.titlebar_spacing
,
804 self
.setLayout(self
.main_layout
)
806 connect_button(self
.toggle_button
, self
.toggle_floating
)
807 connect_button(self
.close_button
, self
.toggle_visibility
)
809 def toggle_floating(self
):
810 self
.parent().setFloating(not self
.parent().isFloating())
811 self
.update_tooltips()
813 def toggle_visibility(self
):
814 self
.parent().toggleViewAction().trigger()
816 def set_title(self
, title
):
817 self
.label
.setText(title
)
819 def add_corner_widget(self
, widget
):
820 self
.corner_layout
.addWidget(widget
)
822 def update_tooltips(self
):
823 if self
.parent().isFloating():
824 tooltip
= N_('Attach')
826 tooltip
= N_('Detach')
827 self
.toggle_button
.setToolTip(tooltip
)
830 def create_dock(name
, title
, parent
, stretch
=True, widget
=None, fn
=None):
831 """Create a dock widget and set it up accordingly."""
832 dock
= QtWidgets
.QDockWidget(parent
)
833 dock
.setWindowTitle(title
)
834 dock
.setObjectName(name
)
835 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
836 dock
.setTitleBarWidget(titlebar
)
837 dock
.setAutoFillBackground(True)
838 if hasattr(parent
, 'dockwidgets'):
839 parent
.dockwidgets
.append(dock
)
842 assert isinstance(widget
, QtWidgets
.QFrame
), "Docked widget has to be a QFrame"
844 dock
.setWidget(widget
)
848 def hide_dock(widget
):
849 widget
.toggleViewAction().setChecked(False)
853 def create_menu(title
, parent
):
854 """Create a menu and set its title."""
855 qmenu
= DebouncingMenu(title
, parent
)
859 class DebouncingMenu(QtWidgets
.QMenu
):
860 """Menu that debounces mouse release action ie. stops it if occurred
861 right after menu creation.
863 Disables annoying behaviour when RMB is pressed to show menu, cursor is
864 moved accidentally 1px onto newly created menu and released causing to
870 def __init__(self
, title
, parent
):
871 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
872 self
.created_at
= utils
.epoch_millis()
873 if hasattr(self
, 'setToolTipsVisible'):
874 self
.setToolTipsVisible(True)
876 def mouseReleaseEvent(self
, event
):
877 threshold
= DebouncingMenu
.threshold_ms
878 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
879 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
882 def add_menu(title
, parent
):
883 """Create a menu and set its title."""
884 menu
= create_menu(title
, parent
)
885 if hasattr(parent
, 'addMenu'):
888 parent
.addAction(menu
.menuAction())
892 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
893 button
= tool_button()
896 button
.setIconSize(QtCore
.QSize(defs
.default_icon
, defs
.default_icon
))
898 button
.setText(' ' + text
)
899 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
900 if tooltip
is not None:
901 button
.setToolTip(tooltip
)
902 if layout
is not None:
903 layout
.addWidget(button
)
907 def create_toolbutton_with_callback(callback
, text
, icon
, tooltip
, layout
=None):
908 """Create a toolbutton that runs the specified callback"""
909 toolbutton
= create_toolbutton(text
=text
, layout
=layout
, tooltip
=tooltip
, icon
=icon
)
910 connect_button(toolbutton
, callback
)
914 # pylint: disable=line-too-long
915 def mimedata_from_paths(context
, paths
, include_urls
=True):
916 """Return mimedata with a list of absolute path URLs
918 Set `include_urls` to False to prevent URLs from being included
919 in the mimedata. This is useful in some terminals that do not gracefully handle
920 multiple URLs being included in the payload.
922 This allows the mimedata to contain just plain a plain text value that we
923 are able to format ourselves.
925 Older verisons of gnome-terminal expected a utf-16 encoding, but that
926 behavior is no longer needed.
928 abspaths
= [core
.abspath(path
) for path
in paths
]
929 paths_text
= core
.list2cmdline(abspaths
)
931 # The text/x-moz-list format is always included by Qt, and doing
932 # mimedata.removeFormat('text/x-moz-url') has no effect.
933 # http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
935 # Older versions of gnome-terminal expect utf-16 encoded text, but other terminals,
936 # e.g. terminator, expect utf-8, so use cola.dragencoding to override the default.
937 # NOTE: text/x-moz-url does not seem to be used/needed by modern versions of
938 # gnome-terminal, kitty, and terminator.
939 mimedata
= QtCore
.QMimeData()
940 mimedata
.setText(paths_text
)
942 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
943 encoding
= context
.cfg
.get('cola.dragencoding', 'utf-16')
944 encoded_text
= core
.encode(paths_text
, encoding
=encoding
)
945 mimedata
.setUrls(urls
)
946 mimedata
.setData('text/x-moz-url', encoded_text
)
951 def path_mimetypes(include_urls
=True):
952 """Return a list of mimetypes that we generate"""
955 'text/plain;charset=utf-8',
958 mime_types
.append('text/uri-list')
959 mime_types
.append('text/x-moz-url')
963 class BlockSignals(object):
964 """Context manager for blocking a signals on a widget"""
966 def __init__(self
, *widgets
):
967 self
.widgets
= widgets
971 """Block Qt signals for all of the captured widgets"""
972 self
.values
= [widget
.blockSignals(True) for widget
in self
.widgets
]
975 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
976 """Restore Qt signals when we exit the scope"""
977 for (widget
, value
) in zip(self
.widgets
, self
.values
):
978 widget
.blockSignals(value
)
981 class Channel(QtCore
.QObject
):
982 finished
= Signal(object)
983 result
= Signal(object)
986 class Task(QtCore
.QRunnable
):
987 """Disable auto-deletion to avoid gc issues
989 Python's garbage collector will try to double-free the task
990 once it's finished, so disable Qt's auto-deletion as a workaround.
995 QtCore
.QRunnable
.__init
__(self
)
997 self
.channel
= Channel()
999 self
.setAutoDelete(False)
1002 self
.result
= self
.task()
1003 self
.channel
.result
.emit(self
.result
)
1004 self
.channel
.finished
.emit(self
)
1006 # pylint: disable=no-self-use
1010 def connect(self
, handler
):
1011 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
1014 class SimpleTask(Task
):
1015 """Run a simple callable as a task"""
1017 def __init__(self
, fn
, *args
, **kwargs
):
1022 self
.kwargs
= kwargs
1025 return self
.fn(*self
.args
, **self
.kwargs
)
1028 class RunTask(QtCore
.QObject
):
1029 """Runs QRunnable instances and transfers control when they finish"""
1031 def __init__(self
, parent
=None):
1032 QtCore
.QObject
.__init
__(self
, parent
)
1034 self
.task_details
= {}
1035 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
1036 self
.result_fn
= None
1038 def start(self
, task
, progress
=None, finish
=None, result
=None):
1039 """Start the task and register a callback"""
1040 self
.result_fn
= result
1041 if progress
is not None:
1043 # prevents garbage collection bugs in certain PyQt4 versions
1044 self
.tasks
.append(task
)
1046 self
.task_details
[task_id
] = (progress
, finish
, result
)
1047 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
1048 self
.threadpool
.start(task
)
1050 def finish(self
, task
):
1053 self
.tasks
.remove(task
)
1057 progress
, finish
, result
= self
.task_details
[task_id
]
1058 del self
.task_details
[task_id
]
1060 finish
= progress
= result
= None
1062 if progress
is not None:
1065 if result
is not None:
1068 if finish
is not None:
1072 # Syntax highlighting
1076 color
= QtGui
.QColor()
1077 color
.setRgb(r
, g
, b
)
1081 def rgba(r
, g
, b
, a
=255):
1082 color
= rgb(r
, g
, b
)
1092 """Convert a QColor into an rgb(int, int, int) CSS string"""
1093 return 'rgb(%d, %d, %d)' % (color
.red(), color
.green(), color
.blue())
1097 """Convert a QColor into a hex aabbcc string"""
1098 return '%02x%02x%02x' % (color
.red(), color
.green(), color
.blue())
1101 def hsl(h
, s
, light
):
1102 return QtGui
.QColor
.fromHslF(
1103 utils
.clamp(h
, 0.0, 1.0), utils
.clamp(s
, 0.0, 1.0), utils
.clamp(light
, 0.0, 1.0)
1107 def hsl_css(h
, s
, light
):
1108 return rgb_css(hsl(h
, s
, light
))
1111 def make_format(fg
=None, bg
=None, bold
=False):
1112 fmt
= QtGui
.QTextCharFormat()
1114 fmt
.setForeground(fg
)
1116 fmt
.setBackground(bg
)
1118 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1122 class ImageFormats(object):
1124 # returns a list of QByteArray objects
1125 formats_qba
= QtGui
.QImageReader
.supportedImageFormats()
1126 # portability: python3 data() returns bytes, python2 returns str
1127 decode
= core
.decode
1128 formats
= [decode(x
.data()) for x
in formats_qba
]
1129 self
.extensions
= {'.' + fmt
for fmt
in formats
}
1131 def ok(self
, filename
):
1132 _
, ext
= os
.path
.splitext(filename
)
1133 return ext
.lower() in self
.extensions
1136 def set_scrollbar_values(widget
, hscroll_value
, vscroll_value
):
1137 """Set scrollbars to the specified values"""
1138 hscroll
= widget
.horizontalScrollBar()
1139 if hscroll
and hscroll_value
is not None:
1140 hscroll
.setValue(hscroll_value
)
1142 vscroll
= widget
.verticalScrollBar()
1143 if vscroll
and vscroll_value
is not None:
1144 vscroll
.setValue(vscroll_value
)
1147 def get_scrollbar_values(widget
):
1148 """Return the current (hscroll, vscroll) scrollbar values for a widget"""
1149 hscroll
= widget
.horizontalScrollBar()
1151 hscroll_value
= get(hscroll
)
1153 hscroll_value
= None
1154 vscroll
= widget
.verticalScrollBar()
1156 vscroll_value
= get(vscroll
)
1158 vscroll_value
= None
1159 return (hscroll_value
, vscroll_value
)
1162 def scroll_to_item(widget
, item
):
1163 """Scroll to an item while retaining the horizontal scroll position"""
1165 hscrollbar
= widget
.horizontalScrollBar()
1167 hscroll
= get(hscrollbar
)
1168 widget
.scrollToItem(item
)
1169 if hscroll
is not None:
1170 hscrollbar
.setValue(hscroll
)
1173 def select_item(widget
, item
):
1174 """Scroll to and make a QTreeWidget item selected and current"""
1175 scroll_to_item(widget
, item
)
1176 widget
.setCurrentItem(item
)
1177 item
.setSelected(True)
1180 def get_selected_values(widget
, top_level_idx
, values
):
1181 """Map the selected items under the top-level item to the values list"""
1182 # Get the top-level item
1183 item
= widget
.topLevelItem(top_level_idx
)
1184 return tree_selection(item
, values
)
1187 def get_selected_items(widget
, idx
):
1188 """Return the selected items under the top-level item"""
1189 item
= widget
.topLevelItem(idx
)
1190 return tree_selection_items(item
)