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 splitter 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 combo box 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) combo box 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) combo box 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 link(url
, text
, palette
=None):
286 palette
= QtGui
.QPalette()
288 color
= palette
.color(QtGui
.QPalette
.WindowText
)
289 rgb_color
= f
'rgb({color.red()}, {color.green()}, {color.blue()})'
290 scope
= {'rgb': rgb_color
, 'text': text
, 'url': url
}
294 <a style="font-style: italic; text-decoration: none; color: %(rgb)s;"
303 def add_completer(widget
, items
):
304 """Add simple completion to a widget"""
305 completer
= QtWidgets
.QCompleter(items
, widget
)
306 completer
.setCaseSensitivity(Qt
.CaseInsensitive
)
307 completer
.setCompletionMode(QtWidgets
.QCompleter
.InlineCompletion
)
308 widget
.setCompleter(completer
)
311 def prompt(msg
, title
=None, text
='', parent
=None):
312 """Presents the user with an input widget and returns the input."""
316 parent
= active_window()
317 result
= QtWidgets
.QInputDialog
.getText(
318 parent
, title
, msg
, QtWidgets
.QLineEdit
.Normal
, text
320 return (result
[0], result
[1])
323 def prompt_n(msg
, inputs
):
324 """Presents the user with N input widgets and returns the results"""
325 dialog
= QtWidgets
.QDialog(active_window())
326 dialog
.setWindowModality(Qt
.WindowModal
)
327 dialog
.setWindowTitle(msg
)
331 if len(k
+ v
) > len(long_value
):
334 min_width
= min(720, text_width(dialog
.font(), long_value
) + 100)
335 dialog
.setMinimumWidth(min_width
)
337 ok_b
= ok_button(msg
, enabled
=False)
338 close_b
= close_button()
343 return [pair
[1].text().strip() for pair
in form_widgets
]
345 for name
, value
in inputs
:
346 lineedit
= QtWidgets
.QLineEdit()
347 # Enable the OK button only when all fields have been populated
348 lineedit
.textChanged
.connect(
349 lambda x
: ok_b
.setEnabled(all(get_values())), type=Qt
.QueuedConnection
352 lineedit
.setText(value
)
353 form_widgets
.append((name
, lineedit
))
356 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
357 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, STRETCH
, close_b
, ok_b
)
358 main_layout
= vbox(defs
.margin
, defs
.button_spacing
, form_layout
, button_layout
)
359 dialog
.setLayout(main_layout
)
362 connect_button(ok_b
, dialog
.accept
)
363 connect_button(close_b
, dialog
.reject
)
365 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
367 success
= accepted
and all(text
)
368 return (success
, text
)
371 def standard_item_type_value(value
):
372 """Return a custom UserType for use in QTreeWidgetItem.type() overrides"""
373 return custom_item_type_value(QtGui
.QStandardItem
, value
)
376 def graphics_item_type_value(value
):
377 """Return a custom UserType for use in QGraphicsItem.type() overrides"""
378 return custom_item_type_value(QtWidgets
.QGraphicsItem
, value
)
381 def custom_item_type_value(cls
, value
):
382 """Return a custom cls.UserType for use in cls.type() overrides"""
383 user_type
= enum_value(cls
.UserType
)
384 return user_type
+ value
387 def enum_value(value
):
388 """Qt6 has enums with an inner '.value' attribute."""
389 if hasattr(value
, 'value'):
394 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
395 TYPE
= standard_item_type_value(101)
397 def __init__(self
, path
, icon
, deleted
):
398 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
400 self
.deleted
= deleted
401 self
.setIcon(0, icons
.from_name(icon
))
402 self
.setText(0, path
)
408 def paths_from_indexes(model
, indexes
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
409 """Return paths from a list of QStandardItemModel indexes"""
410 items
= [model
.itemFromIndex(i
) for i
in indexes
]
411 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
414 def _true_filter(_value
):
418 def paths_from_items(items
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
419 """Return a list of paths from a list of items"""
420 if item_filter
is None:
421 item_filter
= _true_filter
422 return [i
.path
for i
in items
if i
.type() == item_type
and item_filter(i
)]
425 def tree_selection(tree_item
, items
):
426 """Returns an array of model items that correspond to the selected
427 QTreeWidgetItem children"""
429 count
= min(tree_item
.childCount(), len(items
))
430 for idx
in range(count
):
431 if tree_item
.child(idx
).isSelected():
432 selected
.append(items
[idx
])
437 def tree_selection_items(tree_item
):
438 """Returns selected widget items"""
440 for idx
in range(tree_item
.childCount()):
441 child
= tree_item
.child(idx
)
442 if child
.isSelected():
443 selected
.append(child
)
448 def selected_item(list_widget
, items
):
449 """Returns the model item that corresponds to the selected QListWidget
451 widget_items
= list_widget
.selectedItems()
454 widget_item
= widget_items
[0]
455 row
= list_widget
.row(widget_item
)
463 def selected_items(list_widget
, items
):
464 """Returns an array of model items that correspond to the selected
466 item_count
= len(items
)
468 for widget_item
in list_widget
.selectedItems():
469 row
= list_widget
.row(widget_item
)
471 selected
.append(items
[row
])
475 def open_file(title
, directory
=None):
476 """Creates an Open File dialog and returns a filename."""
477 result
= compat
.getopenfilename(
478 parent
=active_window(), caption
=title
, basedir
=directory
483 def open_files(title
, directory
=None, filters
=''):
484 """Creates an Open File dialog and returns a list of filenames."""
485 result
= compat
.getopenfilenames(
486 parent
=active_window(), caption
=title
, basedir
=directory
, filters
=filters
491 def _enum_value(value
):
492 """Resolve Qt6 enum values"""
493 if hasattr(value
, 'value'):
498 def opendir_dialog(caption
, path
):
499 """Prompts for a directory path"""
500 options
= QtWidgets
.QFileDialog
.Option(
501 _enum_value(QtWidgets
.QFileDialog
.Directory
)
502 |
_enum_value(QtWidgets
.QFileDialog
.DontResolveSymlinks
)
503 |
_enum_value(QtWidgets
.QFileDialog
.ReadOnly
)
504 |
_enum_value(QtWidgets
.QFileDialog
.ShowDirsOnly
)
506 return compat
.getexistingdirectory(
507 parent
=active_window(), caption
=caption
, basedir
=path
, options
=options
511 def save_as(filename
, title
='Save As...'):
512 """Creates a Save File dialog and returns a filename."""
513 result
= compat
.getsavefilename(
514 parent
=active_window(), caption
=title
, basedir
=filename
519 def existing_file(directory
, title
='Append...'):
520 """Creates a Save File dialog and returns a filename."""
521 result
= compat
.getopenfilename(
522 parent
=active_window(), caption
=title
, basedir
=directory
527 def copy_path(filename
, absolute
=True):
528 """Copy a filename to the clipboard"""
532 filename
= core
.abspath(filename
)
533 set_clipboard(filename
)
536 def set_clipboard(text
):
537 """Sets the copy/paste buffer to text."""
540 clipboard
= QtWidgets
.QApplication
.clipboard()
541 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
542 if not utils
.is_darwin() and not utils
.is_win32():
543 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
547 def persist_clipboard():
548 """Persist the clipboard
550 X11 stores only a reference to the clipboard data.
551 Send a clipboard event to force a copy of the clipboard to occur.
552 This ensures that the clipboard is present after git-cola exits.
553 Otherwise, the reference is destroyed on exit.
555 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
558 clipboard
= QtWidgets
.QApplication
.clipboard()
559 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
560 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
563 def add_action_bool(widget
, text
, func
, checked
, *shortcuts
):
565 action
= _add_action(widget
, text
, tip
, func
, connect_action_bool
, *shortcuts
)
566 action
.setCheckable(True)
567 action
.setChecked(checked
)
571 def add_action(widget
, text
, func
, *shortcuts
):
572 """Create a QAction and bind it to the `func` callback and hotkeys"""
574 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
577 def add_action_with_icon(widget
, icon
, text
, func
, *shortcuts
):
578 """Create a QAction using a custom icon bound to the `func` callback and hotkeys"""
580 action
= _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
585 def add_action_with_tooltip(widget
, text
, tip
, func
, *shortcuts
):
586 """Create an action with a tooltip"""
587 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
590 def menu_separator(widget
, text
=''):
591 """Return a QAction whose isSeparator() returns true. Used in context menus"""
592 action
= QtWidgets
.QAction(text
, widget
)
593 action
.setSeparator(True)
597 def _add_action(widget
, text
, tip
, func
, connect
, *shortcuts
):
598 action
= QtWidgets
.QAction(text
, widget
)
599 if hasattr(action
, 'setIconVisibleInMenu'):
600 action
.setIconVisibleInMenu(True)
602 action
.setStatusTip(tip
)
603 connect(action
, func
)
605 action
.setShortcuts(shortcuts
)
606 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
607 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
608 widget
.addAction(action
)
612 def set_selected_item(widget
, idx
):
613 """Sets the currently selected item to the item at index idx."""
614 if isinstance(widget
, QtWidgets
.QTreeWidget
):
615 item
= widget
.topLevelItem(idx
)
617 item
.setSelected(True)
618 widget
.setCurrentItem(item
)
621 def add_items(widget
, items
):
622 """Adds items to a widget."""
629 def set_items(widget
, items
):
630 """Clear the existing widget contents and set the new items."""
632 add_items(widget
, items
)
635 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
636 """Given a filename, return a TreeWidgetItem for a status widget
638 "staged", "deleted, and "untracked" control which icon is used.
641 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
642 icon
= icons
.name_from_basename(icon_name
)
643 return TreeWidgetItem(filename
, icon
, deleted
=deleted
)
646 def add_close_action(widget
):
647 """Adds close action and shortcuts to a widget."""
648 return add_action(widget
, N_('Close...'), widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
652 """Return the current application"""
653 return QtWidgets
.QApplication
.instance()
657 rect
= app().primaryScreen().geometry()
658 return (rect
.width(), rect
.height())
661 def center_on_screen(widget
):
662 """Move widget to the center of the default screen"""
663 width
, height
= desktop_size()
664 center_x
= width
// 2
665 center_y
= height
// 2
666 widget
.move(center_x
- widget
.width() // 2, center_y
- widget
.height() // 2)
669 def default_size(parent
, width
, height
, use_parent_height
=True):
670 """Return the parent's size, or the provided defaults"""
671 if parent
is not None:
672 width
= parent
.width()
673 if use_parent_height
:
674 height
= parent
.height()
675 return (width
, height
)
678 def default_monospace_font():
679 if utils
.is_darwin():
681 elif utils
.is_win32():
685 mfont
= QtGui
.QFont()
686 mfont
.setFamily(family
)
690 def diff_font_str(context
):
692 font_str
= cfg
.get(prefs
.FONTDIFF
)
694 font_str
= default_monospace_font().toString()
698 def diff_font(context
):
699 return font_from_string(diff_font_str(context
))
702 def font_from_string(string
):
703 qfont
= QtGui
.QFont()
704 qfont
.fromString(string
)
709 text
='', layout
=None, tooltip
=None, icon
=None, enabled
=True, default
=False
711 """Create a button, set its title, and add it to the parent."""
712 button
= QtWidgets
.QPushButton()
713 button
.setCursor(Qt
.PointingHandCursor
)
714 button
.setFocusPolicy(Qt
.NoFocus
)
716 button
.setText(' ' + text
)
719 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
720 if tooltip
is not None:
721 button
.setToolTip(tooltip
)
722 if layout
is not None:
723 layout
.addWidget(button
)
725 button
.setEnabled(False)
727 button
.setDefault(True)
732 """Create a flat border-less button"""
733 button
= QtWidgets
.QToolButton()
734 button
.setPopupMode(QtWidgets
.QToolButton
.InstantPopup
)
735 button
.setCursor(Qt
.PointingHandCursor
)
736 button
.setFocusPolicy(Qt
.NoFocus
)
738 palette
= QtGui
.QPalette()
739 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
740 highlight_rgb
= rgb_css(highlight
)
742 button
.setStyleSheet(
747 background-color: none;
749 /* Hide the menu indicator */
750 QToolButton::menu-indicator {
754 border: %(border)spx solid %(highlight_rgb)s;
758 'border': defs
.border
,
759 'highlight_rgb': highlight_rgb
,
765 def create_action_button(tooltip
=None, icon
=None, visible
=None):
766 """Create a small tool button for use in dock title widgets"""
767 button
= tool_button()
768 if tooltip
is not None:
769 button
.setToolTip(tooltip
)
772 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
773 if visible
is not None:
774 button
.setVisible(visible
)
778 def ok_button(text
, default
=True, enabled
=True, icon
=None):
781 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
784 def close_button(text
=None, icon
=None):
785 text
= text
or N_('Close')
786 icon
= icons
.mkicon(icon
, icons
.close
)
787 return create_button(text
=text
, icon
=icon
)
790 def edit_button(enabled
=True, default
=False):
791 return create_button(
792 text
=N_('Edit'), icon
=icons
.edit(), enabled
=enabled
, default
=default
796 def refresh_button(enabled
=True, default
=False):
797 return create_button(
798 text
=N_('Refresh'), icon
=icons
.sync(), enabled
=enabled
, default
=default
802 def checkbox(text
='', tooltip
='', checked
=None):
803 """Create a checkbox"""
804 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
807 def radio(text
='', tooltip
='', checked
=None):
808 """Create a radio button"""
809 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
812 def _checkbox(cls
, text
, tooltip
, checked
):
813 """Create a widget and apply properties"""
818 widget
.setToolTip(tooltip
)
819 if checked
is not None:
820 widget
.setChecked(checked
)
824 class DockTitleBarWidget(QtWidgets
.QFrame
):
825 def __init__(self
, parent
, title
, stretch
=True):
826 QtWidgets
.QFrame
.__init
__(self
, parent
)
827 self
.setAutoFillBackground(True)
828 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
829 qfont
= qlabel
.font()
831 qlabel
.setFont(qfont
)
832 qlabel
.setCursor(Qt
.OpenHandCursor
)
834 self
.close_button
= create_action_button(
835 tooltip
=N_('Close'), icon
=icons
.close()
838 self
.toggle_button
= create_action_button(
839 tooltip
=N_('Detach'), icon
=icons
.external()
842 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
843 self
.title_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, qlabel
)
850 self
.main_layout
= hbox(
852 defs
.titlebar_spacing
,
859 self
.setLayout(self
.main_layout
)
861 connect_button(self
.toggle_button
, self
.toggle_floating
)
862 connect_button(self
.close_button
, self
.toggle_visibility
)
864 def toggle_floating(self
):
865 self
.parent().setFloating(not self
.parent().isFloating())
866 self
.update_tooltips()
868 def toggle_visibility(self
):
869 self
.parent().toggleViewAction().trigger()
871 def set_title(self
, title
):
872 self
.label
.setText(title
)
874 def add_title_widget(self
, widget
):
875 """Add widgets to the title area"""
876 self
.title_layout
.addWidget(widget
)
878 def add_corner_widget(self
, widget
):
879 """Add widgets to the corner area"""
880 self
.corner_layout
.addWidget(widget
)
882 def update_tooltips(self
):
883 if self
.parent().isFloating():
884 tooltip
= N_('Attach')
886 tooltip
= N_('Detach')
887 self
.toggle_button
.setToolTip(tooltip
)
890 def create_dock(name
, title
, parent
, stretch
=True, widget
=None, func
=None):
891 """Create a dock widget and set it up accordingly."""
892 dock
= QtWidgets
.QDockWidget(parent
)
893 dock
.setWindowTitle(title
)
894 dock
.setObjectName(name
)
895 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
896 dock
.setTitleBarWidget(titlebar
)
897 dock
.setAutoFillBackground(True)
898 if hasattr(parent
, 'dockwidgets'):
899 parent
.dockwidgets
.append(dock
)
903 dock
.setWidget(widget
)
907 def hide_dock(widget
):
908 widget
.toggleViewAction().setChecked(False)
912 def create_menu(title
, parent
):
913 """Create a menu and set its title."""
914 qmenu
= DebouncingMenu(title
, parent
)
918 class DebouncingMenu(QtWidgets
.QMenu
):
919 """Menu that debounces mouse release action i.e. stops it if occurred
920 right after menu creation.
922 Disables annoying behaviour when RMB is pressed to show menu, cursor is
923 moved accidentally 1 px onto newly created menu and released causing to
929 def __init__(self
, title
, parent
):
930 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
931 self
.created_at
= utils
.epoch_millis()
932 if hasattr(self
, 'setToolTipsVisible'):
933 self
.setToolTipsVisible(True)
935 def mouseReleaseEvent(self
, event
):
936 threshold
= DebouncingMenu
.threshold_ms
937 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
938 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
941 def add_menu(title
, parent
):
942 """Create a menu and set its title."""
943 menu
= create_menu(title
, parent
)
944 if hasattr(parent
, 'addMenu'):
947 parent
.addAction(menu
.menuAction())
951 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
952 button
= tool_button()
955 button
.setIconSize(QtCore
.QSize(defs
.default_icon
, defs
.default_icon
))
957 button
.setText(' ' + text
)
958 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
959 if tooltip
is not None:
960 button
.setToolTip(tooltip
)
961 if layout
is not None:
962 layout
.addWidget(button
)
966 def create_toolbutton_with_callback(callback
, text
, icon
, tooltip
, layout
=None):
967 """Create a tool button that runs the specified callback"""
968 toolbutton
= create_toolbutton(text
=text
, layout
=layout
, tooltip
=tooltip
, icon
=icon
)
969 connect_button(toolbutton
, callback
)
973 def mimedata_from_paths(context
, paths
, include_urls
=True):
974 """Return mime data with a list of absolute path URLs
976 Set `include_urls` to False to prevent URLs from being included
977 in the mime data. This is useful in some terminals that do not gracefully handle
978 multiple URLs being included in the payload.
980 This allows the mime data to contain just plain a plain text value that we
981 are able to format ourselves.
983 Older versions of gnome-terminal expected a UTF-16 encoding, but that
984 behavior is no longer needed.
986 abspaths
= [core
.abspath(path
) for path
in paths
]
987 paths_text
= core
.list2cmdline(abspaths
)
989 # The text/x-moz-list format is always included by Qt, and doing
990 # mimedata.removeFormat('text/x-moz-url') has no effect.
991 # http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
993 # Older versions of gnome-terminal expect UTF-16 encoded text, but other terminals,
994 # e.g. terminator, expect UTF-8, so use cola.dragencoding to override the default.
995 # NOTE: text/x-moz-url does not seem to be used/needed by modern versions of
996 # gnome-terminal, kitty, and terminator.
997 mimedata
= QtCore
.QMimeData()
998 mimedata
.setText(paths_text
)
1000 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
1001 encoding
= context
.cfg
.get('cola.dragencoding', 'utf-16')
1002 encoded_text
= core
.encode(paths_text
, encoding
=encoding
)
1003 mimedata
.setUrls(urls
)
1004 mimedata
.setData('text/x-moz-url', encoded_text
)
1009 def path_mimetypes(include_urls
=True):
1010 """Return a list of mime types that we generate"""
1013 'text/plain;charset=utf-8',
1016 mime_types
.append('text/uri-list')
1017 mime_types
.append('text/x-moz-url')
1022 """Context manager for blocking a signals on a widget"""
1024 def __init__(self
, *widgets
):
1025 self
.widgets
= widgets
1028 def __enter__(self
):
1029 """Block Qt signals for all of the captured widgets"""
1030 self
.values
= [widget
.blockSignals(True) for widget
in self
.widgets
]
1033 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1034 """Restore Qt signals when we exit the scope"""
1035 for widget
, value
in zip(self
.widgets
, self
.values
):
1036 widget
.blockSignals(value
)
1039 class Channel(QtCore
.QObject
):
1040 finished
= Signal(object)
1041 result
= Signal(object)
1044 class Task(QtCore
.QRunnable
):
1045 """Run a task in the background and return the result using a Channel"""
1048 QtCore
.QRunnable
.__init
__(self
)
1050 self
.channel
= Channel()
1052 # Python's garbage collector will try to double-free the task
1053 # once it's finished so disable the Qt auto-deletion.
1054 self
.setAutoDelete(False)
1057 self
.result
= self
.task()
1058 self
.channel
.result
.emit(self
.result
)
1059 self
.channel
.finished
.emit(self
)
1062 """Perform a long-running task"""
1065 def connect(self
, handler
):
1066 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
1069 class SimpleTask(Task
):
1070 """Run a simple callable as a task"""
1072 def __init__(self
, func
, *args
, **kwargs
):
1077 self
.kwargs
= kwargs
1080 return self
.func(*self
.args
, **self
.kwargs
)
1083 class RunTask(QtCore
.QObject
):
1084 """Runs QRunnable instances and transfers control when they finish"""
1086 def __init__(self
, parent
=None):
1087 QtCore
.QObject
.__init
__(self
, parent
)
1089 self
.task_details
= {}
1090 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
1091 self
.result_func
= None
1093 def start(self
, task
, progress
=None, finish
=None, result
=None):
1094 """Start the task and register a callback"""
1095 self
.result_func
= result
1096 if progress
is not None:
1097 if hasattr(progress
, 'start'):
1100 # prevents garbage collection bugs in certain PyQt4 versions
1101 self
.tasks
.append(task
)
1103 self
.task_details
[task_id
] = (progress
, finish
, result
)
1104 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
1105 self
.threadpool
.start(task
)
1107 def finish(self
, task
):
1108 """The task has finished. Run the finish and result callbacks"""
1111 self
.tasks
.remove(task
)
1115 progress
, finish
, result
= self
.task_details
[task_id
]
1116 del self
.task_details
[task_id
]
1118 finish
= progress
= result
= None
1120 if progress
is not None:
1121 if hasattr(progress
, 'stop'):
1125 if result
is not None:
1128 if finish
is not None:
1132 """Wait until all tasks have finished processing"""
1133 self
.threadpool
.waitForDone()
1136 # Syntax highlighting
1139 def rgb(red
, green
, blue
):
1140 """Create a QColor from r, g, b arguments"""
1141 color
= QtGui
.QColor()
1142 color
.setRgb(red
, green
, blue
)
1146 def rgba(red
, green
, blue
, alpha
=255):
1147 """Create a QColor with alpha from r, g, b, a arguments"""
1148 color
= rgb(red
, green
, blue
)
1149 color
.setAlpha(alpha
)
1153 def rgb_triple(args
):
1154 """Create a QColor from an argument with an [r, g, b] triple"""
1159 """Convert a QColor into an rgb #abcdef CSS string"""
1160 return '#%s' % rgb_hex(color
)
1164 """Convert a QColor into a hex aabbcc string"""
1165 return f
'{color.red():02x}{color.green():02x}{color.blue():02x}'
1168 def clamp_color(value
):
1169 """Clamp an integer value between 0 and 255"""
1170 return min(255, max(value
, 0))
1173 def css_color(value
):
1174 """Convert a #abcdef hex string into a QColor"""
1175 if value
.startswith('#'):
1178 red
= clamp_color(int(value
[:2], base
=16)) # ab
1182 green
= clamp_color(int(value
[2:4], base
=16)) # cd
1186 blue
= clamp_color(int(value
[4:6], base
=16)) # ef
1189 return rgb(red
, green
, blue
)
1192 def hsl(hue
, saturation
, lightness
):
1193 """Return a QColor from an hue, saturation and lightness"""
1194 return QtGui
.QColor
.fromHslF(
1195 utils
.clamp(hue
, 0.0, 1.0),
1196 utils
.clamp(saturation
, 0.0, 1.0),
1197 utils
.clamp(lightness
, 0.0, 1.0),
1201 def hsl_css(hue
, saturation
, lightness
):
1202 """Convert HSL values to a CSS #abcdef color string"""
1203 return rgb_css(hsl(hue
, saturation
, lightness
))
1206 def make_format(foreground
=None, background
=None, bold
=False):
1207 """Create a QTextFormat from the provided foreground, background and bold values"""
1208 fmt
= QtGui
.QTextCharFormat()
1210 fmt
.setForeground(foreground
)
1212 fmt
.setBackground(background
)
1214 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1220 # returns a list of QByteArray objects
1221 formats_qba
= QtGui
.QImageReader
.supportedImageFormats()
1222 # portability: python3 data() returns bytes, python2 returns str
1223 decode
= core
.decode
1224 formats
= [decode(x
.data()) for x
in formats_qba
]
1225 self
.extensions
= {'.' + fmt
for fmt
in formats
}
1227 def ok(self
, filename
):
1228 _
, ext
= os
.path
.splitext(filename
)
1229 return ext
.lower() in self
.extensions
1232 def set_scrollbar_values(widget
, hscroll_value
, vscroll_value
):
1233 """Set scrollbars to the specified values"""
1234 hscroll
= widget
.horizontalScrollBar()
1235 if hscroll
and hscroll_value
is not None:
1236 hscroll
.setValue(hscroll_value
)
1238 vscroll
= widget
.verticalScrollBar()
1239 if vscroll
and vscroll_value
is not None:
1240 vscroll
.setValue(vscroll_value
)
1243 def get_scrollbar_values(widget
):
1244 """Return the current (hscroll, vscroll) scrollbar values for a widget"""
1245 hscroll
= widget
.horizontalScrollBar()
1247 hscroll_value
= get(hscroll
)
1249 hscroll_value
= None
1250 vscroll
= widget
.verticalScrollBar()
1252 vscroll_value
= get(vscroll
)
1254 vscroll_value
= None
1255 return (hscroll_value
, vscroll_value
)
1258 def scroll_to_item(widget
, item
):
1259 """Scroll to an item while retaining the horizontal scroll position"""
1261 hscrollbar
= widget
.horizontalScrollBar()
1263 hscroll
= get(hscrollbar
)
1264 widget
.scrollToItem(item
)
1265 if hscroll
is not None:
1266 hscrollbar
.setValue(hscroll
)
1269 def select_item(widget
, item
):
1270 """Scroll to and make a QTreeWidget item selected and current"""
1271 scroll_to_item(widget
, item
)
1272 widget
.setCurrentItem(item
)
1273 item
.setSelected(True)
1276 def get_selected_values(widget
, top_level_idx
, values
):
1277 """Map the selected items under the top-level item to the values list"""
1278 # Get the top-level item
1279 item
= widget
.topLevelItem(top_level_idx
)
1280 return tree_selection(item
, values
)
1283 def get_selected_items(widget
, idx
):
1284 """Return the selected items under the top-level item"""
1285 item
= widget
.topLevelItem(idx
)
1286 return tree_selection_items(item
)
1289 def add_menu_actions(menu
, menu_actions
):
1290 """Add actions to a menu, treating None as a separator"""
1291 current_actions
= menu
.actions()
1293 first_action
= current_actions
[0]
1298 for action
in menu_actions
:
1300 action
= menu_separator(menu
)
1301 menu
.insertAction(first_action
, action
)
1304 def fontmetrics_width(metrics
, text
):
1305 """Get the width in pixels of specified text
1307 Calls QFontMetrics.horizontalAdvance() when available.
1308 QFontMetricswidth() is deprecated. Qt 5.11 added horizontalAdvance().
1310 if hasattr(metrics
, 'horizontalAdvance'):
1311 return metrics
.horizontalAdvance(text
)
1312 return metrics
.width(text
)
1315 def text_width(font
, text
):
1316 """Get the width in pixels for the QFont and text"""
1317 metrics
= QtGui
.QFontMetrics(font
)
1318 return fontmetrics_width(metrics
, text
)
1321 def text_size(font
, text
):
1322 """Return the width in pixels for the specified text
1324 :param font_or_widget: The QFont or widget providing the font to use.
1325 :param text: The text to measure.
1327 metrics
= QtGui
.QFontMetrics(font
)
1328 return (fontmetrics_width(metrics
, text
), metrics
.height())