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 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 # pylint: disable=no-member
349 lineedit
.textChanged
.connect(
350 lambda x
: ok_b
.setEnabled(all(get_values())), type=Qt
.QueuedConnection
353 lineedit
.setText(value
)
354 form_widgets
.append((name
, lineedit
))
357 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
358 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, STRETCH
, close_b
, ok_b
)
359 main_layout
= vbox(defs
.margin
, defs
.button_spacing
, form_layout
, button_layout
)
360 dialog
.setLayout(main_layout
)
363 connect_button(ok_b
, dialog
.accept
)
364 connect_button(close_b
, dialog
.reject
)
366 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
368 success
= accepted
and all(text
)
369 return (success
, text
)
372 def standard_item_type_value(value
):
373 """Return a custom UserType for use in QTreeWidgetItem.type() overrides"""
374 return custom_item_type_value(QtGui
.QStandardItem
, value
)
377 def graphics_item_type_value(value
):
378 """Return a custom UserType for use in QGraphicsItem.type() overrides"""
379 return custom_item_type_value(QtWidgets
.QGraphicsItem
, value
)
382 def custom_item_type_value(cls
, value
):
383 """Return a custom cls.UserType for use in cls.type() overrides"""
384 user_type
= enum_value(cls
.UserType
)
385 return user_type
+ value
388 def enum_value(value
):
389 """Qt6 has enums with an inner '.value' attribute."""
390 if hasattr(value
, 'value'):
395 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
396 TYPE
= standard_item_type_value(101)
398 def __init__(self
, path
, icon
, deleted
):
399 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
401 self
.deleted
= deleted
402 self
.setIcon(0, icons
.from_name(icon
))
403 self
.setText(0, path
)
409 def paths_from_indexes(model
, indexes
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
410 """Return paths from a list of QStandardItemModel indexes"""
411 items
= [model
.itemFromIndex(i
) for i
in indexes
]
412 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
415 def _true_filter(_value
):
419 def paths_from_items(items
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
420 """Return a list of paths from a list of items"""
421 if item_filter
is None:
422 item_filter
= _true_filter
423 return [i
.path
for i
in items
if i
.type() == item_type
and item_filter(i
)]
426 def tree_selection(tree_item
, items
):
427 """Returns an array of model items that correspond to the selected
428 QTreeWidgetItem children"""
430 count
= min(tree_item
.childCount(), len(items
))
431 for idx
in range(count
):
432 if tree_item
.child(idx
).isSelected():
433 selected
.append(items
[idx
])
438 def tree_selection_items(tree_item
):
439 """Returns selected widget items"""
441 for idx
in range(tree_item
.childCount()):
442 child
= tree_item
.child(idx
)
443 if child
.isSelected():
444 selected
.append(child
)
449 def selected_item(list_widget
, items
):
450 """Returns the model item that corresponds to the selected QListWidget
452 widget_items
= list_widget
.selectedItems()
455 widget_item
= widget_items
[0]
456 row
= list_widget
.row(widget_item
)
464 def selected_items(list_widget
, items
):
465 """Returns an array of model items that correspond to the selected
467 item_count
= len(items
)
469 for widget_item
in list_widget
.selectedItems():
470 row
= list_widget
.row(widget_item
)
472 selected
.append(items
[row
])
476 def open_file(title
, directory
=None):
477 """Creates an Open File dialog and returns a filename."""
478 result
= compat
.getopenfilename(
479 parent
=active_window(), caption
=title
, basedir
=directory
484 def open_files(title
, directory
=None, filters
=''):
485 """Creates an Open File dialog and returns a list of filenames."""
486 result
= compat
.getopenfilenames(
487 parent
=active_window(), caption
=title
, basedir
=directory
, filters
=filters
492 def _enum_value(value
):
493 """Resolve Qt6 enum values"""
494 if hasattr(value
, 'value'):
499 def opendir_dialog(caption
, path
):
500 """Prompts for a directory path"""
501 options
= QtWidgets
.QFileDialog
.Option(
502 _enum_value(QtWidgets
.QFileDialog
.Directory
)
503 |
_enum_value(QtWidgets
.QFileDialog
.DontResolveSymlinks
)
504 |
_enum_value(QtWidgets
.QFileDialog
.ReadOnly
)
505 |
_enum_value(QtWidgets
.QFileDialog
.ShowDirsOnly
)
507 return compat
.getexistingdirectory(
508 parent
=active_window(), caption
=caption
, basedir
=path
, options
=options
512 def save_as(filename
, title
='Save As...'):
513 """Creates a Save File dialog and returns a filename."""
514 result
= compat
.getsavefilename(
515 parent
=active_window(), caption
=title
, basedir
=filename
520 def existing_file(directory
, title
='Append...'):
521 """Creates a Save File dialog and returns a filename."""
522 result
= compat
.getopenfilename(
523 parent
=active_window(), caption
=title
, basedir
=directory
528 def copy_path(filename
, absolute
=True):
529 """Copy a filename to the clipboard"""
533 filename
= core
.abspath(filename
)
534 set_clipboard(filename
)
537 def set_clipboard(text
):
538 """Sets the copy/paste buffer to text."""
541 clipboard
= QtWidgets
.QApplication
.clipboard()
542 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
543 if not utils
.is_darwin() and not utils
.is_win32():
544 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
548 # pylint: disable=line-too-long
549 def persist_clipboard():
550 """Persist the clipboard
552 X11 stores only a reference to the clipboard data.
553 Send a clipboard event to force a copy of the clipboard to occur.
554 This ensures that the clipboard is present after git-cola exits.
555 Otherwise, the reference is destroyed on exit.
557 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
560 clipboard
= QtWidgets
.QApplication
.clipboard()
561 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
562 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
565 def add_action_bool(widget
, text
, func
, checked
, *shortcuts
):
567 action
= _add_action(widget
, text
, tip
, func
, connect_action_bool
, *shortcuts
)
568 action
.setCheckable(True)
569 action
.setChecked(checked
)
573 def add_action(widget
, text
, func
, *shortcuts
):
574 """Create a QAction and bind it to the `func` callback and hotkeys"""
576 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
579 def add_action_with_icon(widget
, icon
, text
, func
, *shortcuts
):
580 """Create a QAction using a custom icon bound to the `func` callback and hotkeys"""
582 action
= _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
587 def add_action_with_tooltip(widget
, text
, tip
, func
, *shortcuts
):
588 """Create an action with a tooltip"""
589 return _add_action(widget
, text
, tip
, func
, connect_action
, *shortcuts
)
592 def menu_separator(widget
, text
=''):
593 """Return a QAction whose isSeparator() returns true. Used in context menus"""
594 action
= QtWidgets
.QAction(text
, widget
)
595 action
.setSeparator(True)
599 def _add_action(widget
, text
, tip
, func
, connect
, *shortcuts
):
600 action
= QtWidgets
.QAction(text
, widget
)
601 if hasattr(action
, 'setIconVisibleInMenu'):
602 action
.setIconVisibleInMenu(True)
604 action
.setStatusTip(tip
)
605 connect(action
, func
)
607 action
.setShortcuts(shortcuts
)
608 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
609 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
610 widget
.addAction(action
)
614 def set_selected_item(widget
, idx
):
615 """Sets a the currently selected item to the item at index idx."""
616 if isinstance(widget
, QtWidgets
.QTreeWidget
):
617 item
= widget
.topLevelItem(idx
)
619 item
.setSelected(True)
620 widget
.setCurrentItem(item
)
623 def add_items(widget
, items
):
624 """Adds items to a widget."""
631 def set_items(widget
, items
):
632 """Clear the existing widget contents and set the new items."""
634 add_items(widget
, items
)
637 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
638 """Given a filename, return a TreeWidgetItem for a status widget
640 "staged", "deleted, and "untracked" control which icon is used.
643 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
644 icon
= icons
.name_from_basename(icon_name
)
645 return TreeWidgetItem(filename
, icon
, deleted
=deleted
)
648 def add_close_action(widget
):
649 """Adds close action and shortcuts to a widget."""
650 return add_action(widget
, N_('Close...'), widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
654 """Return the current application"""
655 return QtWidgets
.QApplication
.instance()
659 rect
= app().primaryScreen().geometry()
660 return (rect
.width(), rect
.height())
663 def center_on_screen(widget
):
664 """Move widget to the center of the default screen"""
665 width
, height
= desktop_size()
666 center_x
= width
// 2
667 center_y
= height
// 2
668 widget
.move(center_x
- widget
.width() // 2, center_y
- widget
.height() // 2)
671 def default_size(parent
, width
, height
, use_parent_height
=True):
672 """Return the parent's size, or the provided defaults"""
673 if parent
is not None:
674 width
= parent
.width()
675 if use_parent_height
:
676 height
= parent
.height()
677 return (width
, height
)
680 def default_monospace_font():
681 if utils
.is_darwin():
683 elif utils
.is_win32():
687 mfont
= QtGui
.QFont()
688 mfont
.setFamily(family
)
692 def diff_font_str(context
):
694 font_str
= cfg
.get(prefs
.FONTDIFF
)
696 font_str
= default_monospace_font().toString()
700 def diff_font(context
):
701 return font_from_string(diff_font_str(context
))
704 def font_from_string(string
):
705 qfont
= QtGui
.QFont()
706 qfont
.fromString(string
)
711 text
='', layout
=None, tooltip
=None, icon
=None, enabled
=True, default
=False
713 """Create a button, set its title, and add it to the parent."""
714 button
= QtWidgets
.QPushButton()
715 button
.setCursor(Qt
.PointingHandCursor
)
716 button
.setFocusPolicy(Qt
.NoFocus
)
718 button
.setText(' ' + text
)
721 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
722 if tooltip
is not None:
723 button
.setToolTip(tooltip
)
724 if layout
is not None:
725 layout
.addWidget(button
)
727 button
.setEnabled(False)
729 button
.setDefault(True)
734 """Create a flat border-less button"""
735 button
= QtWidgets
.QToolButton()
736 button
.setPopupMode(QtWidgets
.QToolButton
.InstantPopup
)
737 button
.setCursor(Qt
.PointingHandCursor
)
738 button
.setFocusPolicy(Qt
.NoFocus
)
740 palette
= QtGui
.QPalette()
741 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
742 highlight_rgb
= rgb_css(highlight
)
744 button
.setStyleSheet(
749 background-color: none;
751 /* Hide the menu indicator */
752 QToolButton::menu-indicator {
756 border: %(border)spx solid %(highlight_rgb)s;
760 'border': defs
.border
,
761 'highlight_rgb': highlight_rgb
,
767 def create_action_button(tooltip
=None, icon
=None, visible
=None):
768 """Create a small toolbutton for use in dock title widgets"""
769 button
= tool_button()
770 if tooltip
is not None:
771 button
.setToolTip(tooltip
)
774 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
775 if visible
is not None:
776 button
.setVisible(visible
)
780 def ok_button(text
, default
=True, enabled
=True, icon
=None):
783 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
786 def close_button(text
=None, icon
=None):
787 text
= text
or N_('Close')
788 icon
= icons
.mkicon(icon
, icons
.close
)
789 return create_button(text
=text
, icon
=icon
)
792 def edit_button(enabled
=True, default
=False):
793 return create_button(
794 text
=N_('Edit'), icon
=icons
.edit(), enabled
=enabled
, default
=default
798 def refresh_button(enabled
=True, default
=False):
799 return create_button(
800 text
=N_('Refresh'), icon
=icons
.sync(), enabled
=enabled
, default
=default
804 def checkbox(text
='', tooltip
='', checked
=None):
805 """Create a checkbox"""
806 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
809 def radio(text
='', tooltip
='', checked
=None):
810 """Create a radio button"""
811 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
814 def _checkbox(cls
, text
, tooltip
, checked
):
815 """Create a widget and apply properties"""
820 widget
.setToolTip(tooltip
)
821 if checked
is not None:
822 widget
.setChecked(checked
)
826 class DockTitleBarWidget(QtWidgets
.QFrame
):
827 def __init__(self
, parent
, title
, stretch
=True):
828 QtWidgets
.QFrame
.__init
__(self
, parent
)
829 self
.setAutoFillBackground(True)
830 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
831 qfont
= qlabel
.font()
833 qlabel
.setFont(qfont
)
834 qlabel
.setCursor(Qt
.OpenHandCursor
)
836 self
.close_button
= create_action_button(
837 tooltip
=N_('Close'), icon
=icons
.close()
840 self
.toggle_button
= create_action_button(
841 tooltip
=N_('Detach'), icon
=icons
.external()
844 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
845 self
.title_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, qlabel
)
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_title_widget(self
, widget
):
877 """Add widgets to the title area"""
878 self
.title_layout
.addWidget(widget
)
880 def add_corner_widget(self
, widget
):
881 """Add widgets to the corner area"""
882 self
.corner_layout
.addWidget(widget
)
884 def update_tooltips(self
):
885 if self
.parent().isFloating():
886 tooltip
= N_('Attach')
888 tooltip
= N_('Detach')
889 self
.toggle_button
.setToolTip(tooltip
)
892 def create_dock(name
, title
, parent
, stretch
=True, widget
=None, func
=None):
893 """Create a dock widget and set it up accordingly."""
894 dock
= QtWidgets
.QDockWidget(parent
)
895 dock
.setWindowTitle(title
)
896 dock
.setObjectName(name
)
897 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
898 dock
.setTitleBarWidget(titlebar
)
899 dock
.setAutoFillBackground(True)
900 if hasattr(parent
, 'dockwidgets'):
901 parent
.dockwidgets
.append(dock
)
905 dock
.setWidget(widget
)
909 def hide_dock(widget
):
910 widget
.toggleViewAction().setChecked(False)
914 def create_menu(title
, parent
):
915 """Create a menu and set its title."""
916 qmenu
= DebouncingMenu(title
, parent
)
920 class DebouncingMenu(QtWidgets
.QMenu
):
921 """Menu that debounces mouse release action ie. stops it if occurred
922 right after menu creation.
924 Disables annoying behaviour when RMB is pressed to show menu, cursor is
925 moved accidentally 1px onto newly created menu and released causing to
931 def __init__(self
, title
, parent
):
932 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
933 self
.created_at
= utils
.epoch_millis()
934 if hasattr(self
, 'setToolTipsVisible'):
935 self
.setToolTipsVisible(True)
937 def mouseReleaseEvent(self
, event
):
938 threshold
= DebouncingMenu
.threshold_ms
939 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
940 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
943 def add_menu(title
, parent
):
944 """Create a menu and set its title."""
945 menu
= create_menu(title
, parent
)
946 if hasattr(parent
, 'addMenu'):
949 parent
.addAction(menu
.menuAction())
953 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
954 button
= tool_button()
957 button
.setIconSize(QtCore
.QSize(defs
.default_icon
, defs
.default_icon
))
959 button
.setText(' ' + text
)
960 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
961 if tooltip
is not None:
962 button
.setToolTip(tooltip
)
963 if layout
is not None:
964 layout
.addWidget(button
)
968 def create_toolbutton_with_callback(callback
, text
, icon
, tooltip
, layout
=None):
969 """Create a toolbutton that runs the specified callback"""
970 toolbutton
= create_toolbutton(text
=text
, layout
=layout
, tooltip
=tooltip
, icon
=icon
)
971 connect_button(toolbutton
, callback
)
975 # pylint: disable=line-too-long
976 def mimedata_from_paths(context
, paths
, include_urls
=True):
977 """Return mimedata with a list of absolute path URLs
979 Set `include_urls` to False to prevent URLs from being included
980 in the mimedata. This is useful in some terminals that do not gracefully handle
981 multiple URLs being included in the payload.
983 This allows the mimedata to contain just plain a plain text value that we
984 are able to format ourselves.
986 Older verisons of gnome-terminal expected a utf-16 encoding, but that
987 behavior is no longer needed.
989 abspaths
= [core
.abspath(path
) for path
in paths
]
990 paths_text
= core
.list2cmdline(abspaths
)
992 # The text/x-moz-list format is always included by Qt, and doing
993 # mimedata.removeFormat('text/x-moz-url') has no effect.
994 # http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
996 # Older versions of gnome-terminal expect utf-16 encoded text, but other terminals,
997 # e.g. terminator, expect utf-8, so use cola.dragencoding to override the default.
998 # NOTE: text/x-moz-url does not seem to be used/needed by modern versions of
999 # gnome-terminal, kitty, and terminator.
1000 mimedata
= QtCore
.QMimeData()
1001 mimedata
.setText(paths_text
)
1003 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
1004 encoding
= context
.cfg
.get('cola.dragencoding', 'utf-16')
1005 encoded_text
= core
.encode(paths_text
, encoding
=encoding
)
1006 mimedata
.setUrls(urls
)
1007 mimedata
.setData('text/x-moz-url', encoded_text
)
1012 def path_mimetypes(include_urls
=True):
1013 """Return a list of mimetypes that we generate"""
1016 'text/plain;charset=utf-8',
1019 mime_types
.append('text/uri-list')
1020 mime_types
.append('text/x-moz-url')
1025 """Context manager for blocking a signals on a widget"""
1027 def __init__(self
, *widgets
):
1028 self
.widgets
= widgets
1031 def __enter__(self
):
1032 """Block Qt signals for all of the captured widgets"""
1033 self
.values
= [widget
.blockSignals(True) for widget
in self
.widgets
]
1036 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1037 """Restore Qt signals when we exit the scope"""
1038 for widget
, value
in zip(self
.widgets
, self
.values
):
1039 widget
.blockSignals(value
)
1042 class Channel(QtCore
.QObject
):
1043 finished
= Signal(object)
1044 result
= Signal(object)
1047 class Task(QtCore
.QRunnable
):
1048 """Run a task in the background and return the result using a Channel"""
1051 QtCore
.QRunnable
.__init
__(self
)
1053 self
.channel
= Channel()
1055 # Python's garbage collector will try to double-free the task
1056 # once it's finished, so disable Qt's auto-deletion as a workaround.
1057 self
.setAutoDelete(False)
1060 self
.result
= self
.task()
1061 self
.channel
.result
.emit(self
.result
)
1062 self
.channel
.finished
.emit(self
)
1065 """Perform a long-running task"""
1068 def connect(self
, handler
):
1069 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
1072 class SimpleTask(Task
):
1073 """Run a simple callable as a task"""
1075 def __init__(self
, func
, *args
, **kwargs
):
1080 self
.kwargs
= kwargs
1083 return self
.func(*self
.args
, **self
.kwargs
)
1086 class RunTask(QtCore
.QObject
):
1087 """Runs QRunnable instances and transfers control when they finish"""
1089 def __init__(self
, parent
=None):
1090 QtCore
.QObject
.__init
__(self
, parent
)
1092 self
.task_details
= {}
1093 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
1094 self
.result_func
= None
1096 def start(self
, task
, progress
=None, finish
=None, result
=None):
1097 """Start the task and register a callback"""
1098 self
.result_func
= result
1099 if progress
is not None:
1100 if hasattr(progress
, 'start'):
1103 # prevents garbage collection bugs in certain PyQt4 versions
1104 self
.tasks
.append(task
)
1106 self
.task_details
[task_id
] = (progress
, finish
, result
)
1107 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
1108 self
.threadpool
.start(task
)
1110 def finish(self
, task
):
1111 """The task has finished. Run the finish and result callbacks"""
1114 self
.tasks
.remove(task
)
1118 progress
, finish
, result
= self
.task_details
[task_id
]
1119 del self
.task_details
[task_id
]
1121 finish
= progress
= result
= None
1123 if progress
is not None:
1124 if hasattr(progress
, 'stop'):
1128 if result
is not None:
1131 if finish
is not None:
1135 # Syntax highlighting
1138 def rgb(red
, green
, blue
):
1139 """Create a QColor from r, g, b arguments"""
1140 color
= QtGui
.QColor()
1141 color
.setRgb(red
, green
, blue
)
1145 def rgba(red
, green
, blue
, alpha
=255):
1146 """Create a QColor with alpha from r, g, b, a arguments"""
1147 color
= rgb(red
, green
, blue
)
1148 color
.setAlpha(alpha
)
1152 def rgb_triple(args
):
1153 """Create a QColor from an argument with an [r, g, b] triple"""
1158 """Convert a QColor into an rgb #abcdef CSS string"""
1159 return '#%s' % rgb_hex(color
)
1163 """Convert a QColor into a hex aabbcc string"""
1164 return f
'{color.red():02x}{color.green():02x}{color.blue():02x}'
1167 def clamp_color(value
):
1168 """Clamp an integer value between 0 and 255"""
1169 return min(255, max(value
, 0))
1172 def css_color(value
):
1173 """Convert a #abcdef hex string into a QColor"""
1174 if value
.startswith('#'):
1177 red
= clamp_color(int(value
[:2], base
=16)) # ab
1181 green
= clamp_color(int(value
[2:4], base
=16)) # cd
1185 blue
= clamp_color(int(value
[4:6], base
=16)) # ef
1188 return rgb(red
, green
, blue
)
1191 def hsl(hue
, saturation
, lightness
):
1192 """Return a QColor from an hue, saturation and lightness"""
1193 return QtGui
.QColor
.fromHslF(
1194 utils
.clamp(hue
, 0.0, 1.0),
1195 utils
.clamp(saturation
, 0.0, 1.0),
1196 utils
.clamp(lightness
, 0.0, 1.0),
1200 def hsl_css(hue
, saturation
, lightness
):
1201 """Convert HSL values to a CSS #abcdef color string"""
1202 return rgb_css(hsl(hue
, saturation
, lightness
))
1205 def make_format(foreground
=None, background
=None, bold
=False):
1206 """Create a QTextFormat from the provided foreground, background and bold values"""
1207 fmt
= QtGui
.QTextCharFormat()
1209 fmt
.setForeground(foreground
)
1211 fmt
.setBackground(background
)
1213 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1219 # returns a list of QByteArray objects
1220 formats_qba
= QtGui
.QImageReader
.supportedImageFormats()
1221 # portability: python3 data() returns bytes, python2 returns str
1222 decode
= core
.decode
1223 formats
= [decode(x
.data()) for x
in formats_qba
]
1224 self
.extensions
= {'.' + fmt
for fmt
in formats
}
1226 def ok(self
, filename
):
1227 _
, ext
= os
.path
.splitext(filename
)
1228 return ext
.lower() in self
.extensions
1231 def set_scrollbar_values(widget
, hscroll_value
, vscroll_value
):
1232 """Set scrollbars to the specified values"""
1233 hscroll
= widget
.horizontalScrollBar()
1234 if hscroll
and hscroll_value
is not None:
1235 hscroll
.setValue(hscroll_value
)
1237 vscroll
= widget
.verticalScrollBar()
1238 if vscroll
and vscroll_value
is not None:
1239 vscroll
.setValue(vscroll_value
)
1242 def get_scrollbar_values(widget
):
1243 """Return the current (hscroll, vscroll) scrollbar values for a widget"""
1244 hscroll
= widget
.horizontalScrollBar()
1246 hscroll_value
= get(hscroll
)
1248 hscroll_value
= None
1249 vscroll
= widget
.verticalScrollBar()
1251 vscroll_value
= get(vscroll
)
1253 vscroll_value
= None
1254 return (hscroll_value
, vscroll_value
)
1257 def scroll_to_item(widget
, item
):
1258 """Scroll to an item while retaining the horizontal scroll position"""
1260 hscrollbar
= widget
.horizontalScrollBar()
1262 hscroll
= get(hscrollbar
)
1263 widget
.scrollToItem(item
)
1264 if hscroll
is not None:
1265 hscrollbar
.setValue(hscroll
)
1268 def select_item(widget
, item
):
1269 """Scroll to and make a QTreeWidget item selected and current"""
1270 scroll_to_item(widget
, item
)
1271 widget
.setCurrentItem(item
)
1272 item
.setSelected(True)
1275 def get_selected_values(widget
, top_level_idx
, values
):
1276 """Map the selected items under the top-level item to the values list"""
1277 # Get the top-level item
1278 item
= widget
.topLevelItem(top_level_idx
)
1279 return tree_selection(item
, values
)
1282 def get_selected_items(widget
, idx
):
1283 """Return the selected items under the top-level item"""
1284 item
= widget
.topLevelItem(idx
)
1285 return tree_selection_items(item
)
1288 def add_menu_actions(menu
, menu_actions
):
1289 """Add actions to a menu, treating None as a separator"""
1290 current_actions
= menu
.actions()
1292 first_action
= current_actions
[0]
1297 for action
in menu_actions
:
1299 action
= menu_separator(menu
)
1300 menu
.insertAction(first_action
, action
)
1303 def fontmetrics_width(metrics
, text
):
1304 """Get the width in pixels of specified text
1306 Calls QFontMetrics.horizontalAdvance() when available.
1307 QFontMetricswidth() is deprecated. Qt 5.11 added horizontalAdvance().
1309 if hasattr(metrics
, 'horizontalAdvance'):
1310 return metrics
.horizontalAdvance(text
)
1311 return metrics
.width(text
)
1314 def text_width(font
, text
):
1315 """Get the width in pixels for the QFont and text"""
1316 metrics
= QtGui
.QFontMetrics(font
)
1317 return fontmetrics_width(metrics
, text
)
1320 def text_size(font
, text
):
1321 """Return the width in pixels for the specified text
1323 :param font_or_widget: The QFont or widget providing the font to use.
1324 :param text: The text to measure.
1326 metrics
= QtGui
.QFontMetrics(font
)
1327 return (fontmetrics_width(metrics
, text
), metrics
.height())