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
)
846 self
.title_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, qlabel
)
853 self
.main_layout
= hbox(
855 defs
.titlebar_spacing
,
862 self
.setLayout(self
.main_layout
)
864 connect_button(self
.toggle_button
, self
.toggle_floating
)
865 connect_button(self
.close_button
, self
.toggle_visibility
)
867 def toggle_floating(self
):
868 self
.parent().setFloating(not self
.parent().isFloating())
869 self
.update_tooltips()
871 def toggle_visibility(self
):
872 self
.parent().toggleViewAction().trigger()
874 def set_title(self
, title
):
875 self
.label
.setText(title
)
877 def add_title_widget(self
, widget
):
878 """Add widgets to the title area"""
879 self
.title_layout
.addWidget(widget
)
881 def add_corner_widget(self
, widget
):
882 """Add widgets to the corner area"""
883 self
.corner_layout
.addWidget(widget
)
885 def update_tooltips(self
):
886 if self
.parent().isFloating():
887 tooltip
= N_('Attach')
889 tooltip
= N_('Detach')
890 self
.toggle_button
.setToolTip(tooltip
)
893 def create_dock(name
, title
, parent
, stretch
=True, widget
=None, func
=None):
894 """Create a dock widget and set it up accordingly."""
895 dock
= QtWidgets
.QDockWidget(parent
)
896 dock
.setWindowTitle(title
)
897 dock
.setObjectName(name
)
898 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
899 dock
.setTitleBarWidget(titlebar
)
900 dock
.setAutoFillBackground(True)
901 if hasattr(parent
, 'dockwidgets'):
902 parent
.dockwidgets
.append(dock
)
906 dock
.setWidget(widget
)
910 def hide_dock(widget
):
911 widget
.toggleViewAction().setChecked(False)
915 def create_menu(title
, parent
):
916 """Create a menu and set its title."""
917 qmenu
= DebouncingMenu(title
, parent
)
921 class DebouncingMenu(QtWidgets
.QMenu
):
922 """Menu that debounces mouse release action ie. stops it if occurred
923 right after menu creation.
925 Disables annoying behaviour when RMB is pressed to show menu, cursor is
926 moved accidentally 1px onto newly created menu and released causing to
932 def __init__(self
, title
, parent
):
933 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
934 self
.created_at
= utils
.epoch_millis()
935 if hasattr(self
, 'setToolTipsVisible'):
936 self
.setToolTipsVisible(True)
938 def mouseReleaseEvent(self
, event
):
939 threshold
= DebouncingMenu
.threshold_ms
940 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
941 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
944 def add_menu(title
, parent
):
945 """Create a menu and set its title."""
946 menu
= create_menu(title
, parent
)
947 if hasattr(parent
, 'addMenu'):
950 parent
.addAction(menu
.menuAction())
954 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
955 button
= tool_button()
958 button
.setIconSize(QtCore
.QSize(defs
.default_icon
, defs
.default_icon
))
960 button
.setText(' ' + text
)
961 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
962 if tooltip
is not None:
963 button
.setToolTip(tooltip
)
964 if layout
is not None:
965 layout
.addWidget(button
)
969 def create_toolbutton_with_callback(callback
, text
, icon
, tooltip
, layout
=None):
970 """Create a toolbutton that runs the specified callback"""
971 toolbutton
= create_toolbutton(text
=text
, layout
=layout
, tooltip
=tooltip
, icon
=icon
)
972 connect_button(toolbutton
, callback
)
976 # pylint: disable=line-too-long
977 def mimedata_from_paths(context
, paths
, include_urls
=True):
978 """Return mimedata with a list of absolute path URLs
980 Set `include_urls` to False to prevent URLs from being included
981 in the mimedata. This is useful in some terminals that do not gracefully handle
982 multiple URLs being included in the payload.
984 This allows the mimedata to contain just plain a plain text value that we
985 are able to format ourselves.
987 Older verisons of gnome-terminal expected a utf-16 encoding, but that
988 behavior is no longer needed.
990 abspaths
= [core
.abspath(path
) for path
in paths
]
991 paths_text
= core
.list2cmdline(abspaths
)
993 # The text/x-moz-list format is always included by Qt, and doing
994 # mimedata.removeFormat('text/x-moz-url') has no effect.
995 # http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
997 # Older versions of gnome-terminal expect utf-16 encoded text, but other terminals,
998 # e.g. terminator, expect utf-8, so use cola.dragencoding to override the default.
999 # NOTE: text/x-moz-url does not seem to be used/needed by modern versions of
1000 # gnome-terminal, kitty, and terminator.
1001 mimedata
= QtCore
.QMimeData()
1002 mimedata
.setText(paths_text
)
1004 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
1005 encoding
= context
.cfg
.get('cola.dragencoding', 'utf-16')
1006 encoded_text
= core
.encode(paths_text
, encoding
=encoding
)
1007 mimedata
.setUrls(urls
)
1008 mimedata
.setData('text/x-moz-url', encoded_text
)
1013 def path_mimetypes(include_urls
=True):
1014 """Return a list of mimetypes that we generate"""
1017 'text/plain;charset=utf-8',
1020 mime_types
.append('text/uri-list')
1021 mime_types
.append('text/x-moz-url')
1026 """Context manager for blocking a signals on a widget"""
1028 def __init__(self
, *widgets
):
1029 self
.widgets
= widgets
1032 def __enter__(self
):
1033 """Block Qt signals for all of the captured widgets"""
1034 self
.values
= [widget
.blockSignals(True) for widget
in self
.widgets
]
1037 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1038 """Restore Qt signals when we exit the scope"""
1039 for widget
, value
in zip(self
.widgets
, self
.values
):
1040 widget
.blockSignals(value
)
1043 class Channel(QtCore
.QObject
):
1044 finished
= Signal(object)
1045 result
= Signal(object)
1048 class Task(QtCore
.QRunnable
):
1049 """Run a task in the background and return the result using a Channel"""
1052 QtCore
.QRunnable
.__init
__(self
)
1054 self
.channel
= Channel()
1056 # Python's garbage collector will try to double-free the task
1057 # once it's finished, so disable Qt's auto-deletion as a workaround.
1058 self
.setAutoDelete(False)
1061 self
.result
= self
.task()
1062 self
.channel
.result
.emit(self
.result
)
1063 self
.channel
.finished
.emit(self
)
1066 """Perform a long-running task"""
1069 def connect(self
, handler
):
1070 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
1073 class SimpleTask(Task
):
1074 """Run a simple callable as a task"""
1076 def __init__(self
, func
, *args
, **kwargs
):
1081 self
.kwargs
= kwargs
1084 return self
.func(*self
.args
, **self
.kwargs
)
1087 class RunTask(QtCore
.QObject
):
1088 """Runs QRunnable instances and transfers control when they finish"""
1090 def __init__(self
, parent
=None):
1091 QtCore
.QObject
.__init
__(self
, parent
)
1093 self
.task_details
= {}
1094 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
1095 self
.result_func
= None
1097 def start(self
, task
, progress
=None, finish
=None, result
=None):
1098 """Start the task and register a callback"""
1099 self
.result_func
= result
1100 if progress
is not None:
1101 if hasattr(progress
, 'start'):
1104 # prevents garbage collection bugs in certain PyQt4 versions
1105 self
.tasks
.append(task
)
1107 self
.task_details
[task_id
] = (progress
, finish
, result
)
1108 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
1109 self
.threadpool
.start(task
)
1111 def finish(self
, task
):
1112 """The task has finished. Run the finish and result callbacks"""
1115 self
.tasks
.remove(task
)
1119 progress
, finish
, result
= self
.task_details
[task_id
]
1120 del self
.task_details
[task_id
]
1122 finish
= progress
= result
= None
1124 if progress
is not None:
1125 if hasattr(progress
, 'stop'):
1129 if result
is not None:
1132 if finish
is not None:
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
)