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 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 metrics
= QtGui
.QFontMetrics(dialog
.font())
335 min_width
= min(720, metrics
.width(long_value
) + 100)
336 dialog
.setMinimumWidth(min_width
)
338 ok_b
= ok_button(msg
, enabled
=False)
339 close_b
= close_button()
344 return [pair
[1].text().strip() for pair
in form_widgets
]
346 for name
, value
in inputs
:
347 lineedit
= QtWidgets
.QLineEdit()
348 # Enable the OK button only when all fields have been populated
349 # pylint: disable=no-member
350 lineedit
.textChanged
.connect(
351 lambda x
: ok_b
.setEnabled(all(get_values())), type=Qt
.QueuedConnection
354 lineedit
.setText(value
)
355 form_widgets
.append((name
, lineedit
))
358 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
359 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, STRETCH
, close_b
, ok_b
)
360 main_layout
= vbox(defs
.margin
, defs
.button_spacing
, form_layout
, button_layout
)
361 dialog
.setLayout(main_layout
)
364 connect_button(ok_b
, dialog
.accept
)
365 connect_button(close_b
, dialog
.reject
)
367 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
369 success
= accepted
and all(text
)
370 return (success
, text
)
373 def standard_item_type_value(value
):
374 """Return a custom UserType for use in QTreeWidgetItem.type() overrides"""
375 return custom_item_type_value(QtGui
.QStandardItem
, value
)
378 def graphics_item_type_value(value
):
379 """Return a custom UserType for use in QGraphicsItem.type() overrides"""
380 return custom_item_type_value(QtWidgets
.QGraphicsItem
, value
)
383 def custom_item_type_value(cls
, value
):
384 """Return a custom cls.UserType for use in cls.type() overrides"""
385 user_type
= enum_value(cls
.UserType
)
386 return user_type
+ value
389 def enum_value(value
):
390 """Qt6 has enums with an inner '.value' attribute."""
391 if hasattr(value
, 'value'):
396 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
397 TYPE
= standard_item_type_value(101)
399 def __init__(self
, path
, icon
, deleted
):
400 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
402 self
.deleted
= deleted
403 self
.setIcon(0, icons
.from_name(icon
))
404 self
.setText(0, path
)
410 def paths_from_indexes(model
, indexes
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
411 """Return paths from a list of QStandardItemModel indexes"""
412 items
= [model
.itemFromIndex(i
) for i
in indexes
]
413 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
416 def _true_filter(_value
):
420 def paths_from_items(items
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
421 """Return a list of paths from a list of items"""
422 if item_filter
is None:
423 item_filter
= _true_filter
424 return [i
.path
for i
in items
if i
.type() == item_type
and item_filter(i
)]
427 def tree_selection(tree_item
, items
):
428 """Returns an array of model items that correspond to the selected
429 QTreeWidgetItem children"""
431 count
= min(tree_item
.childCount(), len(items
))
432 for idx
in range(count
):
433 if tree_item
.child(idx
).isSelected():
434 selected
.append(items
[idx
])
439 def tree_selection_items(tree_item
):
440 """Returns selected widget items"""
442 for idx
in range(tree_item
.childCount()):
443 child
= tree_item
.child(idx
)
444 if child
.isSelected():
445 selected
.append(child
)
450 def selected_item(list_widget
, items
):
451 """Returns the model item that corresponds to the selected QListWidget
453 widget_items
= list_widget
.selectedItems()
456 widget_item
= widget_items
[0]
457 row
= list_widget
.row(widget_item
)
465 def selected_items(list_widget
, items
):
466 """Returns an array of model items that correspond to the selected
468 item_count
= len(items
)
470 for widget_item
in list_widget
.selectedItems():
471 row
= list_widget
.row(widget_item
)
473 selected
.append(items
[row
])
477 def open_file(title
, directory
=None):
478 """Creates an Open File dialog and returns a filename."""
479 result
= compat
.getopenfilename(
480 parent
=active_window(), caption
=title
, basedir
=directory
485 def open_files(title
, directory
=None, filters
=''):
486 """Creates an Open File dialog and returns a list of filenames."""
487 result
= compat
.getopenfilenames(
488 parent
=active_window(), caption
=title
, basedir
=directory
, filters
=filters
493 def _enum_value(value
):
494 """Resolve Qt6 enum values"""
495 if hasattr(value
, 'value'):
500 def opendir_dialog(caption
, path
):
501 """Prompts for a directory path"""
502 options
= QtWidgets
.QFileDialog
.Option(
503 _enum_value(QtWidgets
.QFileDialog
.Directory
)
504 |
_enum_value(QtWidgets
.QFileDialog
.DontResolveSymlinks
)
505 |
_enum_value(QtWidgets
.QFileDialog
.ReadOnly
)
506 |
_enum_value(QtWidgets
.QFileDialog
.ShowDirsOnly
)
508 return compat
.getexistingdirectory(
509 parent
=active_window(), caption
=caption
, basedir
=path
, options
=options
513 def save_as(filename
, title
='Save As...'):
514 """Creates a Save File dialog and returns a filename."""
515 result
= compat
.getsavefilename(
516 parent
=active_window(), caption
=title
, basedir
=filename
521 def existing_file(directory
, title
='Append...'):
522 """Creates a Save File dialog and returns a filename."""
523 result
= compat
.getopenfilename(
524 parent
=active_window(), caption
=title
, basedir
=directory
529 def copy_path(filename
, absolute
=True):
530 """Copy a filename to the clipboard"""
534 filename
= core
.abspath(filename
)
535 set_clipboard(filename
)
538 def set_clipboard(text
):
539 """Sets the copy/paste buffer to text."""
542 clipboard
= QtWidgets
.QApplication
.clipboard()
543 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
544 if not utils
.is_darwin() and not utils
.is_win32():
545 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
549 # pylint: disable=line-too-long
550 def persist_clipboard():
551 """Persist the clipboard
553 X11 stores only a reference to the clipboard data.
554 Send a clipboard event to force a copy of the clipboard to occur.
555 This ensures that the clipboard is present after git-cola exits.
556 Otherwise, the reference is destroyed on exit.
558 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
561 clipboard
= QtWidgets
.QApplication
.clipboard()
562 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
563 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
566 def add_action_bool(widget
, text
, func
, checked
, *shortcuts
):
568 action
= _add_action(widget
, text
, tip
, func
, connect_action_bool
, *shortcuts
)
569 action
.setCheckable(True)
570 action
.setChecked(checked
)
574 def add_action(widget
, text
, func
, *shortcuts
):
575 """Create a QAction and bind it to the `func` callback and hotkeys"""
577 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
580 def add_action_with_icon(widget
, icon
, text
, func
, *shortcuts
):
581 """Create a QAction using a custom icon bound to the `func` callback and hotkeys"""
583 action
= _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
588 def add_action_with_tooltip(widget
, text
, tip
, func
, *shortcuts
):
589 """Create an action with a tooltip"""
590 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
593 def menu_separator(widget
, text
=''):
594 """Return a QAction whose isSeparator() returns true. Used in context menus"""
595 action
= QtWidgets
.QAction(text
, widget
)
596 action
.setSeparator(True)
600 def _add_action(widget
, text
, tip
, func
, connect
, *shortcuts
):
601 action
= QtWidgets
.QAction(text
, widget
)
602 if hasattr(action
, 'setIconVisibleInMenu'):
603 action
.setIconVisibleInMenu(True)
605 action
.setStatusTip(tip
)
606 connect(action
, func
)
608 action
.setShortcuts(shortcuts
)
609 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
610 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
611 widget
.addAction(action
)
615 def set_selected_item(widget
, idx
):
616 """Sets a the currently selected item to the item at index idx."""
617 if isinstance(widget
, QtWidgets
.QTreeWidget
):
618 item
= widget
.topLevelItem(idx
)
620 item
.setSelected(True)
621 widget
.setCurrentItem(item
)
624 def add_items(widget
, items
):
625 """Adds items to a widget."""
632 def set_items(widget
, items
):
633 """Clear the existing widget contents and set the new items."""
635 add_items(widget
, items
)
638 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
639 """Given a filename, return a TreeWidgetItem for a status widget
641 "staged", "deleted, and "untracked" control which icon is used.
644 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
645 icon
= icons
.name_from_basename(icon_name
)
646 return TreeWidgetItem(filename
, icon
, deleted
=deleted
)
649 def add_close_action(widget
):
650 """Adds close action and shortcuts to a widget."""
651 return add_action(widget
, N_('Close...'), widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
655 """Return the current application"""
656 return QtWidgets
.QApplication
.instance()
660 rect
= app().primaryScreen().geometry()
661 return (rect
.width(), rect
.height())
664 def center_on_screen(widget
):
665 """Move widget to the center of the default screen"""
666 width
, height
= desktop_size()
667 center_x
= width
// 2
668 center_y
= height
// 2
669 widget
.move(center_x
- widget
.width() // 2, center_y
- widget
.height() // 2)
672 def default_size(parent
, width
, height
, use_parent_height
=True):
673 """Return the parent's size, or the provided defaults"""
674 if parent
is not None:
675 width
= parent
.width()
676 if use_parent_height
:
677 height
= parent
.height()
678 return (width
, height
)
681 def default_monospace_font():
682 if utils
.is_darwin():
684 elif utils
.is_win32():
688 mfont
= QtGui
.QFont()
689 mfont
.setFamily(family
)
693 def diff_font_str(context
):
695 font_str
= cfg
.get(prefs
.FONTDIFF
)
697 font_str
= default_monospace_font().toString()
701 def diff_font(context
):
702 return font(diff_font_str(context
))
706 qfont
= QtGui
.QFont()
707 qfont
.fromString(string
)
712 text
='', layout
=None, tooltip
=None, icon
=None, enabled
=True, default
=False
714 """Create a button, set its title, and add it to the parent."""
715 button
= QtWidgets
.QPushButton()
716 button
.setCursor(Qt
.PointingHandCursor
)
717 button
.setFocusPolicy(Qt
.NoFocus
)
719 button
.setText(' ' + text
)
722 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
723 if tooltip
is not None:
724 button
.setToolTip(tooltip
)
725 if layout
is not None:
726 layout
.addWidget(button
)
728 button
.setEnabled(False)
730 button
.setDefault(True)
735 """Create a flat border-less button"""
736 button
= QtWidgets
.QToolButton()
737 button
.setPopupMode(QtWidgets
.QToolButton
.InstantPopup
)
738 button
.setCursor(Qt
.PointingHandCursor
)
739 button
.setFocusPolicy(Qt
.NoFocus
)
741 palette
= QtGui
.QPalette()
742 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
743 highlight_rgb
= rgb_css(highlight
)
745 button
.setStyleSheet(
750 background-color: none;
752 /* Hide the menu indicator */
753 QToolButton::menu-indicator {
757 border: %(border)spx solid %(highlight_rgb)s;
761 'border': defs
.border
,
762 'highlight_rgb': highlight_rgb
,
768 def create_action_button(tooltip
=None, icon
=None, visible
=None):
769 """Create a small toolbutton for use in dock title widgets"""
770 button
= tool_button()
771 if tooltip
is not None:
772 button
.setToolTip(tooltip
)
775 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
776 if visible
is not None:
777 button
.setVisible(visible
)
781 def ok_button(text
, default
=True, enabled
=True, icon
=None):
784 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
787 def close_button(text
=None, icon
=None):
788 text
= text
or N_('Close')
789 icon
= icons
.mkicon(icon
, icons
.close
)
790 return create_button(text
=text
, icon
=icon
)
793 def edit_button(enabled
=True, default
=False):
794 return create_button(
795 text
=N_('Edit'), icon
=icons
.edit(), enabled
=enabled
, default
=default
799 def refresh_button(enabled
=True, default
=False):
800 return create_button(
801 text
=N_('Refresh'), icon
=icons
.sync(), enabled
=enabled
, default
=default
805 def checkbox(text
='', tooltip
='', checked
=None):
806 """Create a checkbox"""
807 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
810 def radio(text
='', tooltip
='', checked
=None):
811 """Create a radio button"""
812 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
815 def _checkbox(cls
, text
, tooltip
, checked
):
816 """Create a widget and apply properties"""
821 widget
.setToolTip(tooltip
)
822 if checked
is not None:
823 widget
.setChecked(checked
)
827 class DockTitleBarWidget(QtWidgets
.QFrame
):
828 def __init__(self
, parent
, title
, stretch
=True):
829 QtWidgets
.QFrame
.__init
__(self
, parent
)
830 self
.setAutoFillBackground(True)
831 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
832 qfont
= qlabel
.font()
834 qlabel
.setFont(qfont
)
835 qlabel
.setCursor(Qt
.OpenHandCursor
)
837 self
.close_button
= create_action_button(
838 tooltip
=N_('Close'), icon
=icons
.close()
841 self
.toggle_button
= create_action_button(
842 tooltip
=N_('Detach'), icon
=icons
.external()
845 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
852 self
.main_layout
= hbox(
854 defs
.titlebar_spacing
,
861 self
.setLayout(self
.main_layout
)
863 connect_button(self
.toggle_button
, self
.toggle_floating
)
864 connect_button(self
.close_button
, self
.toggle_visibility
)
866 def toggle_floating(self
):
867 self
.parent().setFloating(not self
.parent().isFloating())
868 self
.update_tooltips()
870 def toggle_visibility(self
):
871 self
.parent().toggleViewAction().trigger()
873 def set_title(self
, title
):
874 self
.label
.setText(title
)
876 def add_corner_widget(self
, widget
):
877 self
.corner_layout
.addWidget(widget
)
879 def update_tooltips(self
):
880 if self
.parent().isFloating():
881 tooltip
= N_('Attach')
883 tooltip
= N_('Detach')
884 self
.toggle_button
.setToolTip(tooltip
)
887 def create_dock(name
, title
, parent
, stretch
=True, widget
=None, func
=None):
888 """Create a dock widget and set it up accordingly."""
889 dock
= QtWidgets
.QDockWidget(parent
)
890 dock
.setWindowTitle(title
)
891 dock
.setObjectName(name
)
892 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
893 dock
.setTitleBarWidget(titlebar
)
894 dock
.setAutoFillBackground(True)
895 if hasattr(parent
, 'dockwidgets'):
896 parent
.dockwidgets
.append(dock
)
900 dock
.setWidget(widget
)
904 def hide_dock(widget
):
905 widget
.toggleViewAction().setChecked(False)
909 def create_menu(title
, parent
):
910 """Create a menu and set its title."""
911 qmenu
= DebouncingMenu(title
, parent
)
915 class DebouncingMenu(QtWidgets
.QMenu
):
916 """Menu that debounces mouse release action ie. stops it if occurred
917 right after menu creation.
919 Disables annoying behaviour when RMB is pressed to show menu, cursor is
920 moved accidentally 1px onto newly created menu and released causing to
926 def __init__(self
, title
, parent
):
927 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
928 self
.created_at
= utils
.epoch_millis()
929 if hasattr(self
, 'setToolTipsVisible'):
930 self
.setToolTipsVisible(True)
932 def mouseReleaseEvent(self
, event
):
933 threshold
= DebouncingMenu
.threshold_ms
934 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
935 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
938 def add_menu(title
, parent
):
939 """Create a menu and set its title."""
940 menu
= create_menu(title
, parent
)
941 if hasattr(parent
, 'addMenu'):
944 parent
.addAction(menu
.menuAction())
948 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
949 button
= tool_button()
952 button
.setIconSize(QtCore
.QSize(defs
.default_icon
, defs
.default_icon
))
954 button
.setText(' ' + text
)
955 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
956 if tooltip
is not None:
957 button
.setToolTip(tooltip
)
958 if layout
is not None:
959 layout
.addWidget(button
)
963 def create_toolbutton_with_callback(callback
, text
, icon
, tooltip
, layout
=None):
964 """Create a toolbutton that runs the specified callback"""
965 toolbutton
= create_toolbutton(text
=text
, layout
=layout
, tooltip
=tooltip
, icon
=icon
)
966 connect_button(toolbutton
, callback
)
970 # pylint: disable=line-too-long
971 def mimedata_from_paths(context
, paths
, include_urls
=True):
972 """Return mimedata with a list of absolute path URLs
974 Set `include_urls` to False to prevent URLs from being included
975 in the mimedata. This is useful in some terminals that do not gracefully handle
976 multiple URLs being included in the payload.
978 This allows the mimedata to contain just plain a plain text value that we
979 are able to format ourselves.
981 Older verisons of gnome-terminal expected a utf-16 encoding, but that
982 behavior is no longer needed.
984 abspaths
= [core
.abspath(path
) for path
in paths
]
985 paths_text
= core
.list2cmdline(abspaths
)
987 # The text/x-moz-list format is always included by Qt, and doing
988 # mimedata.removeFormat('text/x-moz-url') has no effect.
989 # http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
991 # Older versions of gnome-terminal expect utf-16 encoded text, but other terminals,
992 # e.g. terminator, expect utf-8, so use cola.dragencoding to override the default.
993 # NOTE: text/x-moz-url does not seem to be used/needed by modern versions of
994 # gnome-terminal, kitty, and terminator.
995 mimedata
= QtCore
.QMimeData()
996 mimedata
.setText(paths_text
)
998 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
999 encoding
= context
.cfg
.get('cola.dragencoding', 'utf-16')
1000 encoded_text
= core
.encode(paths_text
, encoding
=encoding
)
1001 mimedata
.setUrls(urls
)
1002 mimedata
.setData('text/x-moz-url', encoded_text
)
1007 def path_mimetypes(include_urls
=True):
1008 """Return a list of mimetypes that we generate"""
1011 'text/plain;charset=utf-8',
1014 mime_types
.append('text/uri-list')
1015 mime_types
.append('text/x-moz-url')
1020 """Context manager for blocking a signals on a widget"""
1022 def __init__(self
, *widgets
):
1023 self
.widgets
= widgets
1026 def __enter__(self
):
1027 """Block Qt signals for all of the captured widgets"""
1028 self
.values
= [widget
.blockSignals(True) for widget
in self
.widgets
]
1031 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1032 """Restore Qt signals when we exit the scope"""
1033 for widget
, value
in zip(self
.widgets
, self
.values
):
1034 widget
.blockSignals(value
)
1037 class Channel(QtCore
.QObject
):
1038 finished
= Signal(object)
1039 result
= Signal(object)
1042 class Task(QtCore
.QRunnable
):
1043 """Run a task in the background and return the result using a Channel"""
1046 QtCore
.QRunnable
.__init
__(self
)
1048 self
.channel
= Channel()
1050 # Python's garbage collector will try to double-free the task
1051 # once it's finished, so disable Qt's auto-deletion as a workaround.
1052 self
.setAutoDelete(False)
1055 self
.result
= self
.task()
1056 self
.channel
.result
.emit(self
.result
)
1057 self
.channel
.finished
.emit(self
)
1060 """Perform a long-running task"""
1063 def connect(self
, handler
):
1064 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
1067 class SimpleTask(Task
):
1068 """Run a simple callable as a task"""
1070 def __init__(self
, func
, *args
, **kwargs
):
1075 self
.kwargs
= kwargs
1078 return self
.func(*self
.args
, **self
.kwargs
)
1081 class RunTask(QtCore
.QObject
):
1082 """Runs QRunnable instances and transfers control when they finish"""
1084 def __init__(self
, parent
=None):
1085 QtCore
.QObject
.__init
__(self
, parent
)
1087 self
.task_details
= {}
1088 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
1089 self
.result_func
= None
1091 def start(self
, task
, progress
=None, finish
=None, result
=None):
1092 """Start the task and register a callback"""
1093 self
.result_func
= result
1094 if progress
is not None:
1095 if hasattr(progress
, 'start'):
1098 # prevents garbage collection bugs in certain PyQt4 versions
1099 self
.tasks
.append(task
)
1101 self
.task_details
[task_id
] = (progress
, finish
, result
)
1102 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
1103 self
.threadpool
.start(task
)
1105 def finish(self
, task
):
1106 """The task has finished. Run the finish and result callbacks"""
1109 self
.tasks
.remove(task
)
1113 progress
, finish
, result
= self
.task_details
[task_id
]
1114 del self
.task_details
[task_id
]
1116 finish
= progress
= result
= None
1118 if progress
is not None:
1119 if hasattr(progress
, 'stop'):
1123 if result
is not None:
1126 if finish
is not None:
1130 # Syntax highlighting
1133 def rgb(red
, green
, blue
):
1134 """Create a QColor from r, g, b arguments"""
1135 color
= QtGui
.QColor()
1136 color
.setRgb(red
, green
, blue
)
1140 def rgba(red
, green
, blue
, alpha
=255):
1141 """Create a QColor with alpha from r, g, b, a arguments"""
1142 color
= rgb(red
, green
, blue
)
1143 color
.setAlpha(alpha
)
1147 def rgb_triple(args
):
1148 """Create a QColor from an argument with an [r, g, b] triple"""
1153 """Convert a QColor into an rgb #abcdef CSS string"""
1154 return '#%s' % rgb_hex(color
)
1158 """Convert a QColor into a hex aabbcc string"""
1159 return f
'{color.red():02x}{color.green():02x}{color.blue():02x}'
1162 def clamp_color(value
):
1163 """Clamp an integer value between 0 and 255"""
1164 return min(255, max(value
, 0))
1167 def css_color(value
):
1168 """Convert a #abcdef hex string into a QColor"""
1169 if value
.startswith('#'):
1172 red
= clamp_color(int(value
[:2], base
=16)) # ab
1176 green
= clamp_color(int(value
[2:4], base
=16)) # cd
1180 blue
= clamp_color(int(value
[4:6], base
=16)) # ef
1183 return rgb(red
, green
, blue
)
1186 def hsl(hue
, saturation
, lightness
):
1187 """Return a QColor from an hue, saturation and lightness"""
1188 return QtGui
.QColor
.fromHslF(
1189 utils
.clamp(hue
, 0.0, 1.0),
1190 utils
.clamp(saturation
, 0.0, 1.0),
1191 utils
.clamp(lightness
, 0.0, 1.0),
1195 def hsl_css(hue
, saturation
, lightness
):
1196 """Convert HSL values to a CSS #abcdef color string"""
1197 return rgb_css(hsl(hue
, saturation
, lightness
))
1200 def make_format(foreground
=None, background
=None, bold
=False):
1201 """Create a QTextFormat from the provided foreground, background and bold values"""
1202 fmt
= QtGui
.QTextCharFormat()
1204 fmt
.setForeground(foreground
)
1206 fmt
.setBackground(background
)
1208 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1214 # returns a list of QByteArray objects
1215 formats_qba
= QtGui
.QImageReader
.supportedImageFormats()
1216 # portability: python3 data() returns bytes, python2 returns str
1217 decode
= core
.decode
1218 formats
= [decode(x
.data()) for x
in formats_qba
]
1219 self
.extensions
= {'.' + fmt
for fmt
in formats
}
1221 def ok(self
, filename
):
1222 _
, ext
= os
.path
.splitext(filename
)
1223 return ext
.lower() in self
.extensions
1226 def set_scrollbar_values(widget
, hscroll_value
, vscroll_value
):
1227 """Set scrollbars to the specified values"""
1228 hscroll
= widget
.horizontalScrollBar()
1229 if hscroll
and hscroll_value
is not None:
1230 hscroll
.setValue(hscroll_value
)
1232 vscroll
= widget
.verticalScrollBar()
1233 if vscroll
and vscroll_value
is not None:
1234 vscroll
.setValue(vscroll_value
)
1237 def get_scrollbar_values(widget
):
1238 """Return the current (hscroll, vscroll) scrollbar values for a widget"""
1239 hscroll
= widget
.horizontalScrollBar()
1241 hscroll_value
= get(hscroll
)
1243 hscroll_value
= None
1244 vscroll
= widget
.verticalScrollBar()
1246 vscroll_value
= get(vscroll
)
1248 vscroll_value
= None
1249 return (hscroll_value
, vscroll_value
)
1252 def scroll_to_item(widget
, item
):
1253 """Scroll to an item while retaining the horizontal scroll position"""
1255 hscrollbar
= widget
.horizontalScrollBar()
1257 hscroll
= get(hscrollbar
)
1258 widget
.scrollToItem(item
)
1259 if hscroll
is not None:
1260 hscrollbar
.setValue(hscroll
)
1263 def select_item(widget
, item
):
1264 """Scroll to and make a QTreeWidget item selected and current"""
1265 scroll_to_item(widget
, item
)
1266 widget
.setCurrentItem(item
)
1267 item
.setSelected(True)
1270 def get_selected_values(widget
, top_level_idx
, values
):
1271 """Map the selected items under the top-level item to the values list"""
1272 # Get the top-level item
1273 item
= widget
.topLevelItem(top_level_idx
)
1274 return tree_selection(item
, values
)
1277 def get_selected_items(widget
, idx
):
1278 """Return the selected items under the top-level item"""
1279 item
= widget
.topLevelItem(idx
)
1280 return tree_selection_items(item
)
1283 def add_menu_actions(menu
, menu_actions
):
1284 """Add actions to a menu, treating None as a separator"""
1285 current_actions
= menu
.actions()
1287 first_action
= current_actions
[0]
1292 for action
in menu_actions
:
1294 action
= menu_separator(menu
)
1295 menu
.insertAction(first_action
, action
)