1 """Miscellaneous Qt utility functions."""
4 from qtpy
import compat
6 from qtpy
import QtCore
7 from qtpy
import QtWidgets
8 from qtpy
.QtCore
import Qt
9 from qtpy
.QtCore
import Signal
16 from .compat
import int_types
17 from .compat
import ustr
18 from .models
import prefs
19 from .widgets
import defs
27 """Return the active window for the current application"""
28 return QtWidgets
.QApplication
.activeWindow()
31 def current_palette():
32 """Return the QPalette for the current application"""
33 return QtWidgets
.QApplication
.instance().palette()
36 def connect_action(action
, func
):
37 """Connect an action to a function"""
38 action
.triggered
[bool].connect(lambda x
: func(), type=Qt
.QueuedConnection
)
41 def connect_action_bool(action
, func
):
42 """Connect a triggered(bool) action to a function"""
43 action
.triggered
[bool].connect(func
, type=Qt
.QueuedConnection
)
46 def connect_button(button
, func
):
47 """Connect a button to a function"""
48 # Some versions of Qt send the `bool` argument to the clicked callback,
49 # and some do not. The lambda consumes all callback-provided arguments.
50 button
.clicked
.connect(lambda *args
, **kwargs
: func(), type=Qt
.QueuedConnection
)
53 def connect_checkbox(widget
, func
):
54 """Connect a checkbox to a function taking bool"""
55 widget
.clicked
.connect(
56 lambda *args
, **kwargs
: func(get(checkbox
)), type=Qt
.QueuedConnection
60 def connect_released(button
, func
):
61 """Connect a button to a function"""
62 button
.released
.connect(func
, type=Qt
.QueuedConnection
)
65 def button_action(button
, action
):
66 """Make a button trigger an action"""
67 connect_button(button
, action
.trigger
)
70 def connect_toggle(toggle
, func
):
71 """Connect a toggle button to a function"""
72 toggle
.toggled
.connect(func
, type=Qt
.QueuedConnection
)
75 def disconnect(signal
):
76 """Disconnect signal from all slots"""
79 except TypeError: # allow unconnected slots
83 def get(widget
, default
=None):
84 """Query a widget for its python value"""
85 if hasattr(widget
, 'isChecked'):
86 value
= widget
.isChecked()
87 elif hasattr(widget
, 'value'):
88 value
= widget
.value()
89 elif hasattr(widget
, 'text'):
91 elif hasattr(widget
, 'toPlainText'):
92 value
= widget
.toPlainText()
93 elif hasattr(widget
, 'sizes'):
94 value
= widget
.sizes()
95 elif hasattr(widget
, 'date'):
96 value
= widget
.date().toString(Qt
.ISODate
)
102 def hbox(margin
, spacing
, *items
):
103 """Create an HBoxLayout with the specified sizes and items"""
104 return box(QtWidgets
.QHBoxLayout
, margin
, spacing
, *items
)
107 def vbox(margin
, spacing
, *items
):
108 """Create a VBoxLayout with the specified sizes and items"""
109 return box(QtWidgets
.QVBoxLayout
, margin
, spacing
, *items
)
112 def buttongroup(*items
):
113 """Create a QButtonGroup for the specified items"""
114 group
= QtWidgets
.QButtonGroup()
120 def set_margin(layout
, margin
):
121 """Set the content margins for a layout"""
122 layout
.setContentsMargins(margin
, margin
, margin
, margin
)
125 def box(cls
, margin
, spacing
, *items
):
126 """Create a QBoxLayout with the specified sizes and items"""
130 layout
.setSpacing(spacing
)
131 set_margin(layout
, margin
)
134 if isinstance(i
, QtWidgets
.QWidget
):
139 QtWidgets
.QHBoxLayout
,
140 QtWidgets
.QVBoxLayout
,
141 QtWidgets
.QFormLayout
,
150 elif isinstance(i
, int_types
):
156 def form(margin
, spacing
, *widgets
):
157 """Create a QFormLayout with the specified sizes and items"""
158 layout
= QtWidgets
.QFormLayout()
159 layout
.setSpacing(spacing
)
160 layout
.setFieldGrowthPolicy(QtWidgets
.QFormLayout
.ExpandingFieldsGrow
)
161 set_margin(layout
, margin
)
163 for idx
, (name
, widget
) in enumerate(widgets
):
164 if isinstance(name
, (str, ustr
)):
165 layout
.addRow(name
, widget
)
167 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.LabelRole
, name
)
168 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.FieldRole
, widget
)
173 def grid(margin
, spacing
, *widgets
):
174 """Create a QGridLayout with the specified sizes and items"""
175 layout
= QtWidgets
.QGridLayout()
176 layout
.setSpacing(spacing
)
177 set_margin(layout
, margin
)
181 if isinstance(item
, QtWidgets
.QWidget
):
182 layout
.addWidget(*row
)
183 elif isinstance(item
, QtWidgets
.QLayoutItem
):
189 def splitter(orientation
, *widgets
):
190 """Create a spliter over the specified widgets
192 :param orientation: Qt.Horizontal or Qt.Vertical
195 layout
= QtWidgets
.QSplitter()
196 layout
.setOrientation(orientation
)
197 layout
.setHandleWidth(defs
.handle_width
)
198 layout
.setChildrenCollapsible(True)
200 for idx
, widget
in enumerate(widgets
):
201 layout
.addWidget(widget
)
202 layout
.setStretchFactor(idx
, 1)
204 # Workaround for Qt not setting the WA_Hover property for QSplitter
205 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
206 layout
.handle(1).setAttribute(Qt
.WA_Hover
)
211 def label(text
=None, align
=None, fmt
=None, selectable
=True):
212 """Create a QLabel with the specified properties"""
213 widget
= QtWidgets
.QLabel()
214 if align
is not None:
215 widget
.setAlignment(align
)
217 widget
.setTextFormat(fmt
)
219 widget
.setTextInteractionFlags(Qt
.TextBrowserInteraction
)
220 widget
.setOpenExternalLinks(True)
226 class ComboBox(QtWidgets
.QComboBox
):
227 """Custom read-only combobox with a convenient API"""
229 def __init__(self
, items
=None, editable
=False, parent
=None, transform
=None):
230 super().__init
__(parent
)
231 self
.setEditable(editable
)
232 self
.transform
= transform
236 self
.item_data
.extend(items
)
238 def set_index(self
, idx
):
239 idx
= utils
.clamp(idx
, 0, self
.count() - 1)
240 self
.setCurrentIndex(idx
)
242 def add_item(self
, text
, data
):
244 self
.item_data
.append(data
)
246 def current_data(self
):
247 return self
.item_data
[self
.currentIndex()]
249 def set_value(self
, value
):
251 value
= self
.transform(value
)
253 index
= self
.item_data
.index(value
)
256 self
.setCurrentIndex(index
)
259 def combo(items
, editable
=False, tooltip
='', parent
=None):
260 """Create a readonly (by default) combobox from a list of items"""
261 combobox
= ComboBox(editable
=editable
, items
=items
, parent
=parent
)
263 combobox
.setToolTip(tooltip
)
267 def combo_mapped(data
, editable
=False, transform
=None, parent
=None):
268 """Create a readonly (by default) combobox from a list of items"""
269 widget
= ComboBox(editable
=editable
, transform
=transform
, parent
=parent
)
271 widget
.add_item(k
, v
)
275 def textbrowser(text
=None):
276 """Create a QTextBrowser for the specified text"""
277 widget
= QtWidgets
.QTextBrowser()
278 widget
.setOpenExternalLinks(True)
284 def add_completer(widget
, items
):
285 """Add simple completion to a widget"""
286 completer
= QtWidgets
.QCompleter(items
, widget
)
287 completer
.setCaseSensitivity(Qt
.CaseInsensitive
)
288 completer
.setCompletionMode(QtWidgets
.QCompleter
.InlineCompletion
)
289 widget
.setCompleter(completer
)
292 def prompt(msg
, title
=None, text
='', parent
=None):
293 """Presents the user with an input widget and returns the input."""
297 parent
= active_window()
298 result
= QtWidgets
.QInputDialog
.getText(
299 parent
, title
, msg
, QtWidgets
.QLineEdit
.Normal
, text
301 return (result
[0], result
[1])
304 def prompt_n(msg
, inputs
):
305 """Presents the user with N input widgets and returns the results"""
306 dialog
= QtWidgets
.QDialog(active_window())
307 dialog
.setWindowModality(Qt
.WindowModal
)
308 dialog
.setWindowTitle(msg
)
312 if len(k
+ v
) > len(long_value
):
315 metrics
= QtGui
.QFontMetrics(dialog
.font())
316 min_width
= min(720, metrics
.width(long_value
) + 100)
317 dialog
.setMinimumWidth(min_width
)
319 ok_b
= ok_button(msg
, enabled
=False)
320 close_b
= close_button()
325 return [pair
[1].text().strip() for pair
in form_widgets
]
327 for name
, value
in inputs
:
328 lineedit
= QtWidgets
.QLineEdit()
329 # Enable the OK button only when all fields have been populated
330 # pylint: disable=no-member
331 lineedit
.textChanged
.connect(
332 lambda x
: ok_b
.setEnabled(all(get_values())), type=Qt
.QueuedConnection
335 lineedit
.setText(value
)
336 form_widgets
.append((name
, lineedit
))
339 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
340 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, STRETCH
, close_b
, ok_b
)
341 main_layout
= vbox(defs
.margin
, defs
.button_spacing
, form_layout
, button_layout
)
342 dialog
.setLayout(main_layout
)
345 connect_button(ok_b
, dialog
.accept
)
346 connect_button(close_b
, dialog
.reject
)
348 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
350 success
= accepted
and all(text
)
351 return (success
, text
)
354 def standard_item_type_value(value
):
355 """Return a custom UserType for use in QTreeWidgetItem.type() overrides"""
356 return custom_item_type_value(QtGui
.QStandardItem
, value
)
359 def graphics_item_type_value(value
):
360 """Return a custom UserType for use in QGraphicsItem.type() overrides"""
361 return custom_item_type_value(QtWidgets
.QGraphicsItem
, value
)
364 def custom_item_type_value(cls
, value
):
365 """Return a custom cls.UserType for use in cls.type() overrides"""
366 user_type
= enum_value(cls
.UserType
)
367 return user_type
+ value
370 def enum_value(value
):
371 """Qt6 has enums with an inner '.value' attribute."""
372 if hasattr(value
, 'value'):
377 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
378 TYPE
= standard_item_type_value(101)
380 def __init__(self
, path
, icon
, deleted
):
381 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
383 self
.deleted
= deleted
384 self
.setIcon(0, icons
.from_name(icon
))
385 self
.setText(0, path
)
391 def paths_from_indexes(model
, indexes
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
392 """Return paths from a list of QStandardItemModel indexes"""
393 items
= [model
.itemFromIndex(i
) for i
in indexes
]
394 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
397 def _true_filter(_value
):
401 def paths_from_items(items
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
402 """Return a list of paths from a list of items"""
403 if item_filter
is None:
404 item_filter
= _true_filter
405 return [i
.path
for i
in items
if i
.type() == item_type
and item_filter(i
)]
408 def tree_selection(tree_item
, items
):
409 """Returns an array of model items that correspond to the selected
410 QTreeWidgetItem children"""
412 count
= min(tree_item
.childCount(), len(items
))
413 for idx
in range(count
):
414 if tree_item
.child(idx
).isSelected():
415 selected
.append(items
[idx
])
420 def tree_selection_items(tree_item
):
421 """Returns selected widget items"""
423 for idx
in range(tree_item
.childCount()):
424 child
= tree_item
.child(idx
)
425 if child
.isSelected():
426 selected
.append(child
)
431 def selected_item(list_widget
, items
):
432 """Returns the model item that corresponds to the selected QListWidget
434 widget_items
= list_widget
.selectedItems()
437 widget_item
= widget_items
[0]
438 row
= list_widget
.row(widget_item
)
446 def selected_items(list_widget
, items
):
447 """Returns an array of model items that correspond to the selected
449 item_count
= len(items
)
451 for widget_item
in list_widget
.selectedItems():
452 row
= list_widget
.row(widget_item
)
454 selected
.append(items
[row
])
458 def open_file(title
, directory
=None):
459 """Creates an Open File dialog and returns a filename."""
460 result
= compat
.getopenfilename(
461 parent
=active_window(), caption
=title
, basedir
=directory
466 def open_files(title
, directory
=None, filters
=''):
467 """Creates an Open File dialog and returns a list of filenames."""
468 result
= compat
.getopenfilenames(
469 parent
=active_window(), caption
=title
, basedir
=directory
, filters
=filters
474 def opendir_dialog(caption
, path
):
475 """Prompts for a directory path"""
477 QtWidgets
.QFileDialog
.Directory
478 | QtWidgets
.QFileDialog
.DontResolveSymlinks
479 | QtWidgets
.QFileDialog
.ReadOnly
480 | QtWidgets
.QFileDialog
.ShowDirsOnly
482 return compat
.getexistingdirectory(
483 parent
=active_window(), caption
=caption
, basedir
=path
, options
=options
487 def save_as(filename
, title
='Save As...'):
488 """Creates a Save File dialog and returns a filename."""
489 result
= compat
.getsavefilename(
490 parent
=active_window(), caption
=title
, basedir
=filename
495 def existing_file(directory
, title
='Append...'):
496 """Creates a Save File dialog and returns a filename."""
497 result
= compat
.getopenfilename(
498 parent
=active_window(), caption
=title
, basedir
=directory
503 def copy_path(filename
, absolute
=True):
504 """Copy a filename to the clipboard"""
508 filename
= core
.abspath(filename
)
509 set_clipboard(filename
)
512 def set_clipboard(text
):
513 """Sets the copy/paste buffer to text."""
516 clipboard
= QtWidgets
.QApplication
.clipboard()
517 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
518 if not utils
.is_darwin() and not utils
.is_win32():
519 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
523 # pylint: disable=line-too-long
524 def persist_clipboard():
525 """Persist the clipboard
527 X11 stores only a reference to the clipboard data.
528 Send a clipboard event to force a copy of the clipboard to occur.
529 This ensures that the clipboard is present after git-cola exits.
530 Otherwise, the reference is destroyed on exit.
532 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
535 clipboard
= QtWidgets
.QApplication
.clipboard()
536 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
537 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
540 def add_action_bool(widget
, text
, func
, checked
, *shortcuts
):
542 action
= _add_action(widget
, text
, tip
, func
, connect_action_bool
, *shortcuts
)
543 action
.setCheckable(True)
544 action
.setChecked(checked
)
548 def add_action(widget
, text
, func
, *shortcuts
):
549 """Create a QAction and bind it to the `func` callback and hotkeys"""
551 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
554 def add_action_with_icon(widget
, icon
, text
, func
, *shortcuts
):
555 """Create a QAction using a custom icon bound to the `func` callback and hotkeys"""
557 action
= _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
562 def add_action_with_tooltip(widget
, text
, tip
, func
, *shortcuts
):
563 """Create an action with a tooltip"""
564 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
567 def menu_separator(widget
, text
=''):
568 """Return a QAction whose isSeparator() returns true. Used in context menus"""
569 action
= QtWidgets
.QAction(text
, widget
)
570 action
.setSeparator(True)
574 def _add_action(widget
, text
, tip
, func
, connect
, *shortcuts
):
575 action
= QtWidgets
.QAction(text
, widget
)
576 if hasattr(action
, 'setIconVisibleInMenu'):
577 action
.setIconVisibleInMenu(True)
579 action
.setStatusTip(tip
)
580 connect(action
, func
)
582 action
.setShortcuts(shortcuts
)
583 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
584 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
585 widget
.addAction(action
)
589 def set_selected_item(widget
, idx
):
590 """Sets a the currently selected item to the item at index idx."""
591 if isinstance(widget
, QtWidgets
.QTreeWidget
):
592 item
= widget
.topLevelItem(idx
)
594 item
.setSelected(True)
595 widget
.setCurrentItem(item
)
598 def add_items(widget
, items
):
599 """Adds items to a widget."""
606 def set_items(widget
, items
):
607 """Clear the existing widget contents and set the new items."""
609 add_items(widget
, items
)
612 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
613 """Given a filename, return a TreeWidgetItem for a status widget
615 "staged", "deleted, and "untracked" control which icon is used.
618 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
619 icon
= icons
.name_from_basename(icon_name
)
620 return TreeWidgetItem(filename
, icon
, deleted
=deleted
)
623 def add_close_action(widget
):
624 """Adds close action and shortcuts to a widget."""
625 return add_action(widget
, N_('Close...'), widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
629 """Return the current application"""
630 return QtWidgets
.QApplication
.instance()
634 rect
= app().primaryScreen().geometry()
635 return (rect
.width(), rect
.height())
638 def center_on_screen(widget
):
639 """Move widget to the center of the default screen"""
640 width
, height
= desktop_size()
641 center_x
= width
// 2
642 center_y
= height
// 2
643 widget
.move(center_x
- widget
.width() // 2, center_y
- widget
.height() // 2)
646 def default_size(parent
, width
, height
, use_parent_height
=True):
647 """Return the parent's size, or the provided defaults"""
648 if parent
is not None:
649 width
= parent
.width()
650 if use_parent_height
:
651 height
= parent
.height()
652 return (width
, height
)
655 def default_monospace_font():
656 if utils
.is_darwin():
660 mfont
= QtGui
.QFont()
661 mfont
.setFamily(family
)
665 def diff_font_str(context
):
667 font_str
= cfg
.get(prefs
.FONTDIFF
)
669 font_str
= default_monospace_font().toString()
673 def diff_font(context
):
674 return font(diff_font_str(context
))
678 qfont
= QtGui
.QFont()
679 qfont
.fromString(string
)
684 text
='', layout
=None, tooltip
=None, icon
=None, enabled
=True, default
=False
686 """Create a button, set its title, and add it to the parent."""
687 button
= QtWidgets
.QPushButton()
688 button
.setCursor(Qt
.PointingHandCursor
)
689 button
.setFocusPolicy(Qt
.NoFocus
)
691 button
.setText(' ' + text
)
694 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
695 if tooltip
is not None:
696 button
.setToolTip(tooltip
)
697 if layout
is not None:
698 layout
.addWidget(button
)
700 button
.setEnabled(False)
702 button
.setDefault(True)
707 """Create a flat border-less button"""
708 button
= QtWidgets
.QToolButton()
709 button
.setPopupMode(QtWidgets
.QToolButton
.InstantPopup
)
710 button
.setCursor(Qt
.PointingHandCursor
)
711 button
.setFocusPolicy(Qt
.NoFocus
)
713 palette
= QtGui
.QPalette()
714 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
715 highlight_rgb
= rgb_css(highlight
)
717 button
.setStyleSheet(
722 background-color: none;
724 /* Hide the menu indicator */
725 QToolButton::menu-indicator {
729 border: %(border)spx solid %(highlight_rgb)s;
733 'border': defs
.border
,
734 'highlight_rgb': highlight_rgb
,
740 def create_action_button(tooltip
=None, icon
=None, visible
=None):
741 """Create a small toolbutton for use in dock title widgets"""
742 button
= tool_button()
743 if tooltip
is not None:
744 button
.setToolTip(tooltip
)
747 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
748 if visible
is not None:
749 button
.setVisible(visible
)
753 def ok_button(text
, default
=True, enabled
=True, icon
=None):
756 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
759 def close_button(text
=None, icon
=None):
760 text
= text
or N_('Close')
761 icon
= icons
.mkicon(icon
, icons
.close
)
762 return create_button(text
=text
, icon
=icon
)
765 def edit_button(enabled
=True, default
=False):
766 return create_button(
767 text
=N_('Edit'), icon
=icons
.edit(), enabled
=enabled
, default
=default
771 def refresh_button(enabled
=True, default
=False):
772 return create_button(
773 text
=N_('Refresh'), icon
=icons
.sync(), enabled
=enabled
, default
=default
777 def checkbox(text
='', tooltip
='', checked
=None):
778 """Create a checkbox"""
779 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
782 def radio(text
='', tooltip
='', checked
=None):
783 """Create a radio button"""
784 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
787 def _checkbox(cls
, text
, tooltip
, checked
):
788 """Create a widget and apply properties"""
793 widget
.setToolTip(tooltip
)
794 if checked
is not None:
795 widget
.setChecked(checked
)
799 class DockTitleBarWidget(QtWidgets
.QFrame
):
800 def __init__(self
, parent
, title
, stretch
=True):
801 QtWidgets
.QFrame
.__init
__(self
, parent
)
802 self
.setAutoFillBackground(True)
803 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
804 qfont
= qlabel
.font()
806 qlabel
.setFont(qfont
)
807 qlabel
.setCursor(Qt
.OpenHandCursor
)
809 self
.close_button
= create_action_button(
810 tooltip
=N_('Close'), icon
=icons
.close()
813 self
.toggle_button
= create_action_button(
814 tooltip
=N_('Detach'), icon
=icons
.external()
817 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
824 self
.main_layout
= hbox(
826 defs
.titlebar_spacing
,
833 self
.setLayout(self
.main_layout
)
835 connect_button(self
.toggle_button
, self
.toggle_floating
)
836 connect_button(self
.close_button
, self
.toggle_visibility
)
838 def toggle_floating(self
):
839 self
.parent().setFloating(not self
.parent().isFloating())
840 self
.update_tooltips()
842 def toggle_visibility(self
):
843 self
.parent().toggleViewAction().trigger()
845 def set_title(self
, title
):
846 self
.label
.setText(title
)
848 def add_corner_widget(self
, widget
):
849 self
.corner_layout
.addWidget(widget
)
851 def update_tooltips(self
):
852 if self
.parent().isFloating():
853 tooltip
= N_('Attach')
855 tooltip
= N_('Detach')
856 self
.toggle_button
.setToolTip(tooltip
)
859 def create_dock(name
, title
, parent
, stretch
=True, widget
=None, func
=None):
860 """Create a dock widget and set it up accordingly."""
861 dock
= QtWidgets
.QDockWidget(parent
)
862 dock
.setWindowTitle(title
)
863 dock
.setObjectName(name
)
864 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
865 dock
.setTitleBarWidget(titlebar
)
866 dock
.setAutoFillBackground(True)
867 if hasattr(parent
, 'dockwidgets'):
868 parent
.dockwidgets
.append(dock
)
872 dock
.setWidget(widget
)
876 def hide_dock(widget
):
877 widget
.toggleViewAction().setChecked(False)
881 def create_menu(title
, parent
):
882 """Create a menu and set its title."""
883 qmenu
= DebouncingMenu(title
, parent
)
887 class DebouncingMenu(QtWidgets
.QMenu
):
888 """Menu that debounces mouse release action ie. stops it if occurred
889 right after menu creation.
891 Disables annoying behaviour when RMB is pressed to show menu, cursor is
892 moved accidentally 1px onto newly created menu and released causing to
898 def __init__(self
, title
, parent
):
899 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
900 self
.created_at
= utils
.epoch_millis()
901 if hasattr(self
, 'setToolTipsVisible'):
902 self
.setToolTipsVisible(True)
904 def mouseReleaseEvent(self
, event
):
905 threshold
= DebouncingMenu
.threshold_ms
906 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
907 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
910 def add_menu(title
, parent
):
911 """Create a menu and set its title."""
912 menu
= create_menu(title
, parent
)
913 if hasattr(parent
, 'addMenu'):
916 parent
.addAction(menu
.menuAction())
920 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
921 button
= tool_button()
924 button
.setIconSize(QtCore
.QSize(defs
.default_icon
, defs
.default_icon
))
926 button
.setText(' ' + text
)
927 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
928 if tooltip
is not None:
929 button
.setToolTip(tooltip
)
930 if layout
is not None:
931 layout
.addWidget(button
)
935 def create_toolbutton_with_callback(callback
, text
, icon
, tooltip
, layout
=None):
936 """Create a toolbutton that runs the specified callback"""
937 toolbutton
= create_toolbutton(text
=text
, layout
=layout
, tooltip
=tooltip
, icon
=icon
)
938 connect_button(toolbutton
, callback
)
942 # pylint: disable=line-too-long
943 def mimedata_from_paths(context
, paths
, include_urls
=True):
944 """Return mimedata with a list of absolute path URLs
946 Set `include_urls` to False to prevent URLs from being included
947 in the mimedata. This is useful in some terminals that do not gracefully handle
948 multiple URLs being included in the payload.
950 This allows the mimedata to contain just plain a plain text value that we
951 are able to format ourselves.
953 Older verisons of gnome-terminal expected a utf-16 encoding, but that
954 behavior is no longer needed.
956 abspaths
= [core
.abspath(path
) for path
in paths
]
957 paths_text
= core
.list2cmdline(abspaths
)
959 # The text/x-moz-list format is always included by Qt, and doing
960 # mimedata.removeFormat('text/x-moz-url') has no effect.
961 # http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
963 # Older versions of gnome-terminal expect utf-16 encoded text, but other terminals,
964 # e.g. terminator, expect utf-8, so use cola.dragencoding to override the default.
965 # NOTE: text/x-moz-url does not seem to be used/needed by modern versions of
966 # gnome-terminal, kitty, and terminator.
967 mimedata
= QtCore
.QMimeData()
968 mimedata
.setText(paths_text
)
970 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
971 encoding
= context
.cfg
.get('cola.dragencoding', 'utf-16')
972 encoded_text
= core
.encode(paths_text
, encoding
=encoding
)
973 mimedata
.setUrls(urls
)
974 mimedata
.setData('text/x-moz-url', encoded_text
)
979 def path_mimetypes(include_urls
=True):
980 """Return a list of mimetypes that we generate"""
983 'text/plain;charset=utf-8',
986 mime_types
.append('text/uri-list')
987 mime_types
.append('text/x-moz-url')
992 """Context manager for blocking a signals on a widget"""
994 def __init__(self
, *widgets
):
995 self
.widgets
= widgets
999 """Block Qt signals for all of the captured widgets"""
1000 self
.values
= [widget
.blockSignals(True) for widget
in self
.widgets
]
1003 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1004 """Restore Qt signals when we exit the scope"""
1005 for widget
, value
in zip(self
.widgets
, self
.values
):
1006 widget
.blockSignals(value
)
1009 class Channel(QtCore
.QObject
):
1010 finished
= Signal(object)
1011 result
= Signal(object)
1014 class Task(QtCore
.QRunnable
):
1015 """Run a task in the background and return the result using a Channel"""
1018 QtCore
.QRunnable
.__init
__(self
)
1020 self
.channel
= Channel()
1022 # Python's garbage collector will try to double-free the task
1023 # once it's finished, so disable Qt's auto-deletion as a workaround.
1024 self
.setAutoDelete(False)
1027 self
.result
= self
.task()
1028 self
.channel
.result
.emit(self
.result
)
1029 self
.channel
.finished
.emit(self
)
1032 """Perform a long-running task"""
1035 def connect(self
, handler
):
1036 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
1039 class SimpleTask(Task
):
1040 """Run a simple callable as a task"""
1042 def __init__(self
, func
, *args
, **kwargs
):
1047 self
.kwargs
= kwargs
1050 return self
.func(*self
.args
, **self
.kwargs
)
1053 class RunTask(QtCore
.QObject
):
1054 """Runs QRunnable instances and transfers control when they finish"""
1056 def __init__(self
, parent
=None):
1057 QtCore
.QObject
.__init
__(self
, parent
)
1059 self
.task_details
= {}
1060 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
1061 self
.result_func
= None
1063 def start(self
, task
, progress
=None, finish
=None, result
=None):
1064 """Start the task and register a callback"""
1065 self
.result_func
= result
1066 if progress
is not None:
1067 if hasattr(progress
, 'start'):
1070 # prevents garbage collection bugs in certain PyQt4 versions
1071 self
.tasks
.append(task
)
1073 self
.task_details
[task_id
] = (progress
, finish
, result
)
1074 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
1075 self
.threadpool
.start(task
)
1077 def finish(self
, task
):
1078 """The task has finished. Run the finish and result callbacks"""
1081 self
.tasks
.remove(task
)
1085 progress
, finish
, result
= self
.task_details
[task_id
]
1086 del self
.task_details
[task_id
]
1088 finish
= progress
= result
= None
1090 if progress
is not None:
1091 if hasattr(progress
, 'stop'):
1095 if result
is not None:
1098 if finish
is not None:
1102 # Syntax highlighting
1105 def rgb(red
, green
, blue
):
1106 """Create a QColor from r, g, b arguments"""
1107 color
= QtGui
.QColor()
1108 color
.setRgb(red
, green
, blue
)
1112 def rgba(red
, green
, blue
, alpha
=255):
1113 """Create a QColor with alpha from r, g, b, a arguments"""
1114 color
= rgb(red
, green
, blue
)
1115 color
.setAlpha(alpha
)
1119 def rgb_triple(args
):
1120 """Create a QColor from an argument with an [r, g, b] triple"""
1125 """Convert a QColor into an rgb #abcdef CSS string"""
1126 return '#%s' % rgb_hex(color
)
1130 """Convert a QColor into a hex aabbcc string"""
1131 return f
'{color.red():02x}{color.green():02x}{color.blue():02x}'
1134 def clamp_color(value
):
1135 """Clamp an integer value between 0 and 255"""
1136 return min(255, max(value
, 0))
1139 def css_color(value
):
1140 """Convert a #abcdef hex string into a QColor"""
1141 if value
.startswith('#'):
1144 red
= clamp_color(int(value
[:2], base
=16)) # ab
1148 green
= clamp_color(int(value
[2:4], base
=16)) # cd
1152 blue
= clamp_color(int(value
[4:6], base
=16)) # ef
1155 return rgb(red
, green
, blue
)
1158 def hsl(hue
, saturation
, lightness
):
1159 """Return a QColor from an hue, saturation and lightness"""
1160 return QtGui
.QColor
.fromHslF(
1161 utils
.clamp(hue
, 0.0, 1.0),
1162 utils
.clamp(saturation
, 0.0, 1.0),
1163 utils
.clamp(lightness
, 0.0, 1.0),
1167 def hsl_css(hue
, saturation
, lightness
):
1168 """Convert HSL values to a CSS #abcdef color string"""
1169 return rgb_css(hsl(hue
, saturation
, lightness
))
1172 def make_format(foreground
=None, background
=None, bold
=False):
1173 """Create a QTextFormat from the provided foreground, background and bold values"""
1174 fmt
= QtGui
.QTextCharFormat()
1176 fmt
.setForeground(foreground
)
1178 fmt
.setBackground(background
)
1180 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1186 # returns a list of QByteArray objects
1187 formats_qba
= QtGui
.QImageReader
.supportedImageFormats()
1188 # portability: python3 data() returns bytes, python2 returns str
1189 decode
= core
.decode
1190 formats
= [decode(x
.data()) for x
in formats_qba
]
1191 self
.extensions
= {'.' + fmt
for fmt
in formats
}
1193 def ok(self
, filename
):
1194 _
, ext
= os
.path
.splitext(filename
)
1195 return ext
.lower() in self
.extensions
1198 def set_scrollbar_values(widget
, hscroll_value
, vscroll_value
):
1199 """Set scrollbars to the specified values"""
1200 hscroll
= widget
.horizontalScrollBar()
1201 if hscroll
and hscroll_value
is not None:
1202 hscroll
.setValue(hscroll_value
)
1204 vscroll
= widget
.verticalScrollBar()
1205 if vscroll
and vscroll_value
is not None:
1206 vscroll
.setValue(vscroll_value
)
1209 def get_scrollbar_values(widget
):
1210 """Return the current (hscroll, vscroll) scrollbar values for a widget"""
1211 hscroll
= widget
.horizontalScrollBar()
1213 hscroll_value
= get(hscroll
)
1215 hscroll_value
= None
1216 vscroll
= widget
.verticalScrollBar()
1218 vscroll_value
= get(vscroll
)
1220 vscroll_value
= None
1221 return (hscroll_value
, vscroll_value
)
1224 def scroll_to_item(widget
, item
):
1225 """Scroll to an item while retaining the horizontal scroll position"""
1227 hscrollbar
= widget
.horizontalScrollBar()
1229 hscroll
= get(hscrollbar
)
1230 widget
.scrollToItem(item
)
1231 if hscroll
is not None:
1232 hscrollbar
.setValue(hscroll
)
1235 def select_item(widget
, item
):
1236 """Scroll to and make a QTreeWidget item selected and current"""
1237 scroll_to_item(widget
, item
)
1238 widget
.setCurrentItem(item
)
1239 item
.setSelected(True)
1242 def get_selected_values(widget
, top_level_idx
, values
):
1243 """Map the selected items under the top-level item to the values list"""
1244 # Get the top-level item
1245 item
= widget
.topLevelItem(top_level_idx
)
1246 return tree_selection(item
, values
)
1249 def get_selected_items(widget
, idx
):
1250 """Return the selected items under the top-level item"""
1251 item
= widget
.topLevelItem(idx
)
1252 return tree_selection_items(item
)
1255 def add_menu_actions(menu
, menu_actions
):
1256 """Add actions to a menu, treating None as a separator"""
1257 current_actions
= menu
.actions()
1259 first_action
= current_actions
[0]
1264 for action
in menu_actions
:
1266 action
= menu_separator(menu
)
1267 menu
.insertAction(first_action
, action
)