1 # Copyright (C) 2007-2018 David Aguilar and contributors
2 """Miscellaneous Qt utility functions."""
3 from __future__
import division
, absolute_import
, unicode_literals
6 from qtpy
import compat
8 from qtpy
import QtCore
9 from qtpy
import QtWidgets
10 from qtpy
.QtCore
import Qt
11 from qtpy
.QtCore
import Signal
18 from .compat
import int_types
19 from .compat
import ustr
20 from .models
import prefs
21 from .widgets
import defs
29 """Return the active window for the current application"""
30 return QtWidgets
.QApplication
.activeWindow()
33 def connect_action(action
, fn
):
34 """Connect an action to a function"""
35 action
.triggered
[bool].connect(lambda x
: fn())
38 def connect_action_bool(action
, fn
):
39 """Connect a triggered(bool) action to a function"""
40 action
.triggered
[bool].connect(fn
)
43 def connect_button(button
, fn
):
44 """Connect a button to a function"""
45 # Some versions of Qt send the `bool` argument to the clicked callback,
46 # and some do not. The lambda consumes all callback-provided arguments.
47 button
.clicked
.connect(lambda *args
, **kwargs
: fn())
50 def connect_checkbox(widget
, fn
):
51 """Connect a checkbox to a function taking bool"""
52 widget
.clicked
.connect(lambda *args
, **kwargs
: fn(get(checkbox
)))
55 def connect_released(button
, fn
):
56 """Connect a button to a function"""
57 button
.released
.connect(fn
)
60 def button_action(button
, action
):
61 """Make a button trigger an action"""
62 connect_button(button
, action
.trigger
)
65 def connect_toggle(toggle
, fn
):
66 """Connect a toggle button to a function"""
67 toggle
.toggled
.connect(fn
)
70 def disconnect(signal
):
71 """Disconnect signal from all slots"""
74 except TypeError: # allow unconnected slots
79 """Query a widget for its python value"""
80 if hasattr(widget
, 'isChecked'):
81 value
= widget
.isChecked()
82 elif hasattr(widget
, 'value'):
83 value
= widget
.value()
84 elif hasattr(widget
, 'text'):
86 elif hasattr(widget
, 'toPlainText'):
87 value
= widget
.toPlainText()
88 elif hasattr(widget
, 'sizes'):
89 value
= widget
.sizes()
90 elif hasattr(widget
, 'date'):
91 value
= widget
.date().toString(Qt
.ISODate
)
97 def hbox(margin
, spacing
, *items
):
98 """Create an HBoxLayout with the specified sizes and items"""
99 return box(QtWidgets
.QHBoxLayout
, margin
, spacing
, *items
)
102 def vbox(margin
, spacing
, *items
):
103 """Create a VBoxLayout with the specified sizes and items"""
104 return box(QtWidgets
.QVBoxLayout
, margin
, spacing
, *items
)
107 def buttongroup(*items
):
108 """Create a QButtonGroup for the specified items"""
109 group
= QtWidgets
.QButtonGroup()
115 def set_margin(layout
, margin
):
116 """Set the content margins for a layout"""
117 layout
.setContentsMargins(margin
, margin
, margin
, margin
)
120 def box(cls
, margin
, spacing
, *items
):
121 """Create a QBoxLayout with the specified sizes and items"""
125 layout
.setSpacing(spacing
)
126 set_margin(layout
, margin
)
129 if isinstance(i
, QtWidgets
.QWidget
):
134 QtWidgets
.QHBoxLayout
,
135 QtWidgets
.QVBoxLayout
,
136 QtWidgets
.QFormLayout
,
145 elif isinstance(i
, int_types
):
151 def form(margin
, spacing
, *widgets
):
152 """Create a QFormLayout with the specified sizes and items"""
153 layout
= QtWidgets
.QFormLayout()
154 layout
.setSpacing(spacing
)
155 layout
.setFieldGrowthPolicy(QtWidgets
.QFormLayout
.ExpandingFieldsGrow
)
156 set_margin(layout
, margin
)
158 for idx
, (name
, widget
) in enumerate(widgets
):
159 if isinstance(name
, (str, ustr
)):
160 layout
.addRow(name
, widget
)
162 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.LabelRole
, name
)
163 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.FieldRole
, widget
)
168 def grid(margin
, spacing
, *widgets
):
169 """Create a QGridLayout with the specified sizes and items"""
170 layout
= QtWidgets
.QGridLayout()
171 layout
.setSpacing(spacing
)
172 set_margin(layout
, margin
)
176 if isinstance(item
, QtWidgets
.QWidget
):
177 layout
.addWidget(*row
)
178 elif isinstance(item
, QtWidgets
.QLayoutItem
):
184 def splitter(orientation
, *widgets
):
185 """Create a spliter over the specified widgets
187 :param orientation: Qt.Horizontal or Qt.Vertical
190 layout
= QtWidgets
.QSplitter()
191 layout
.setOrientation(orientation
)
192 layout
.setHandleWidth(defs
.handle_width
)
193 layout
.setChildrenCollapsible(True)
195 for idx
, widget
in enumerate(widgets
):
196 layout
.addWidget(widget
)
197 layout
.setStretchFactor(idx
, 1)
199 # Workaround for Qt not setting the WA_Hover property for QSplitter
200 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
201 layout
.handle(1).setAttribute(Qt
.WA_Hover
)
206 def label(text
=None, align
=None, fmt
=None, selectable
=True):
207 """Create a QLabel with the specified properties"""
208 widget
= QtWidgets
.QLabel()
209 if align
is not None:
210 widget
.setAlignment(align
)
212 widget
.setTextFormat(fmt
)
214 widget
.setTextInteractionFlags(Qt
.TextBrowserInteraction
)
215 widget
.setOpenExternalLinks(True)
221 class ComboBox(QtWidgets
.QComboBox
):
222 """Custom read-only combobox with a convenient API"""
224 def __init__(self
, items
=None, editable
=False, parent
=None, transform
=None):
225 super(ComboBox
, self
).__init
__(parent
)
226 self
.setEditable(editable
)
227 self
.transform
= transform
231 self
.item_data
.extend(items
)
233 def set_index(self
, idx
):
234 idx
= utils
.clamp(idx
, 0, self
.count() - 1)
235 self
.setCurrentIndex(idx
)
237 def add_item(self
, text
, data
):
239 self
.item_data
.append(data
)
241 def current_data(self
):
242 return self
.item_data
[self
.currentIndex()]
244 def set_value(self
, value
):
246 value
= self
.transform(value
)
248 index
= self
.item_data
.index(value
)
251 self
.setCurrentIndex(index
)
254 def combo(items
, editable
=False, parent
=None):
255 """Create a readonly (by default) combobox from a list of items"""
256 return ComboBox(editable
=editable
, items
=items
, parent
=parent
)
259 def combo_mapped(data
, editable
=False, transform
=None, parent
=None):
260 """Create a readonly (by default) combobox from a list of items"""
261 widget
= ComboBox(editable
=editable
, transform
=transform
, parent
=parent
)
263 widget
.add_item(k
, v
)
267 def textbrowser(text
=None):
268 """Create a QTextBrowser for the specified text"""
269 widget
= QtWidgets
.QTextBrowser()
270 widget
.setOpenExternalLinks(True)
276 def add_completer(widget
, items
):
277 """Add simple completion to a widget"""
278 completer
= QtWidgets
.QCompleter(items
, widget
)
279 completer
.setCaseSensitivity(Qt
.CaseInsensitive
)
280 completer
.setCompletionMode(QtWidgets
.QCompleter
.InlineCompletion
)
281 widget
.setCompleter(completer
)
284 def prompt(msg
, title
=None, text
='', parent
=None):
285 """Presents the user with an input widget and returns the input."""
289 parent
= active_window()
290 result
= QtWidgets
.QInputDialog
.getText(
291 parent
, title
, msg
, QtWidgets
.QLineEdit
.Normal
, text
293 return (result
[0], result
[1])
296 def prompt_n(msg
, inputs
):
297 """Presents the user with N input widgets and returns the results"""
298 dialog
= QtWidgets
.QDialog(active_window())
299 dialog
.setWindowModality(Qt
.WindowModal
)
300 dialog
.setWindowTitle(msg
)
304 if len(k
+ v
) > len(long_value
):
307 metrics
= QtGui
.QFontMetrics(dialog
.font())
308 min_width
= metrics
.width(long_value
) + 100
311 dialog
.setMinimumWidth(min_width
)
313 ok_b
= ok_button(msg
, enabled
=False)
314 close_b
= close_button()
319 return [pair
[1].text().strip() for pair
in form_widgets
]
321 for name
, value
in inputs
:
322 lineedit
= QtWidgets
.QLineEdit()
323 # Enable the OK button only when all fields have been populated
324 # pylint: disable=no-member
325 lineedit
.textChanged
.connect(lambda x
: ok_b
.setEnabled(all(get_values())))
327 lineedit
.setText(value
)
328 form_widgets
.append((name
, lineedit
))
331 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
332 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
, STRETCH
, close_b
, ok_b
)
333 main_layout
= vbox(defs
.margin
, defs
.button_spacing
, form_layout
, button_layout
)
334 dialog
.setLayout(main_layout
)
337 connect_button(ok_b
, dialog
.accept
)
338 connect_button(close_b
, dialog
.reject
)
340 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
342 ok
= accepted
and all(text
)
346 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
348 TYPE
= QtGui
.QStandardItem
.UserType
+ 101
350 def __init__(self
, path
, icon
, deleted
):
351 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
353 self
.deleted
= deleted
354 self
.setIcon(0, icons
.from_name(icon
))
355 self
.setText(0, path
)
361 def paths_from_indexes(model
, indexes
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
362 """Return paths from a list of QStandardItemModel indexes"""
363 items
= [model
.itemFromIndex(i
) for i
in indexes
]
364 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
367 def _true_filter(_x
):
371 def paths_from_items(items
, item_type
=TreeWidgetItem
.TYPE
, item_filter
=None):
372 """Return a list of paths from a list of items"""
373 if item_filter
is None:
374 item_filter
= _true_filter
375 return [i
.path
for i
in items
if i
.type() == item_type
and item_filter(i
)]
378 def tree_selection(tree_item
, items
):
379 """Returns an array of model items that correspond to the selected
380 QTreeWidgetItem children"""
382 count
= min(tree_item
.childCount(), len(items
))
383 for idx
in range(count
):
384 if tree_item
.child(idx
).isSelected():
385 selected
.append(items
[idx
])
390 def tree_selection_items(tree_item
):
391 """Returns selected widget items"""
393 for idx
in range(tree_item
.childCount()):
394 child
= tree_item
.child(idx
)
395 if child
.isSelected():
396 selected
.append(child
)
401 def selected_item(list_widget
, items
):
402 """Returns the model item that corresponds to the selected QListWidget
404 widget_items
= list_widget
.selectedItems()
407 widget_item
= widget_items
[0]
408 row
= list_widget
.row(widget_item
)
416 def selected_items(list_widget
, items
):
417 """Returns an array of model items that correspond to the selected
419 item_count
= len(items
)
421 for widget_item
in list_widget
.selectedItems():
422 row
= list_widget
.row(widget_item
)
424 selected
.append(items
[row
])
428 def open_file(title
, directory
=None):
429 """Creates an Open File dialog and returns a filename."""
430 result
= compat
.getopenfilename(
431 parent
=active_window(), caption
=title
, basedir
=directory
436 def open_files(title
, directory
=None, filters
=''):
437 """Creates an Open File dialog and returns a list of filenames."""
438 result
= compat
.getopenfilenames(
439 parent
=active_window(), caption
=title
, basedir
=directory
, filters
=filters
444 def opendir_dialog(caption
, path
):
445 """Prompts for a directory path"""
448 QtWidgets
.QFileDialog
.ShowDirsOnly | QtWidgets
.QFileDialog
.DontResolveSymlinks
450 return compat
.getexistingdirectory(
451 parent
=active_window(), caption
=caption
, basedir
=path
, options
=options
455 def save_as(filename
, title
='Save As...'):
456 """Creates a Save File dialog and returns a filename."""
457 result
= compat
.getsavefilename(
458 parent
=active_window(), caption
=title
, basedir
=filename
463 def copy_path(filename
, absolute
=True):
464 """Copy a filename to the clipboard"""
468 filename
= core
.abspath(filename
)
469 set_clipboard(filename
)
472 def set_clipboard(text
):
473 """Sets the copy/paste buffer to text."""
476 clipboard
= QtWidgets
.QApplication
.clipboard()
477 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
478 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
482 # pylint: disable=line-too-long
483 def persist_clipboard():
484 """Persist the clipboard
486 X11 stores only a reference to the clipboard data.
487 Send a clipboard event to force a copy of the clipboard to occur.
488 This ensures that the clipboard is present after git-cola exits.
489 Otherwise, the reference is destroyed on exit.
491 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
494 clipboard
= QtWidgets
.QApplication
.clipboard()
495 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
496 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
499 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
501 action
= _add_action(widget
, text
, tip
, fn
, connect_action_bool
, *shortcuts
)
502 action
.setCheckable(True)
503 action
.setChecked(checked
)
507 def add_action(widget
, text
, fn
, *shortcuts
):
509 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
512 def add_action_with_status_tip(widget
, text
, tip
, fn
, *shortcuts
):
513 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
516 def _add_action(widget
, text
, tip
, fn
, connect
, *shortcuts
):
517 action
= QtWidgets
.QAction(text
, widget
)
518 if hasattr(action
, 'setIconVisibleInMenu'):
519 action
.setIconVisibleInMenu(True)
521 action
.setStatusTip(tip
)
524 action
.setShortcuts(shortcuts
)
525 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
526 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
527 widget
.addAction(action
)
531 def set_selected_item(widget
, idx
):
532 """Sets a the currently selected item to the item at index idx."""
533 if isinstance(widget
, QtWidgets
.QTreeWidget
):
534 item
= widget
.topLevelItem(idx
)
536 item
.setSelected(True)
537 widget
.setCurrentItem(item
)
540 def add_items(widget
, items
):
541 """Adds items to a widget."""
548 def set_items(widget
, items
):
549 """Clear the existing widget contents and set the new items."""
551 add_items(widget
, items
)
554 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
555 """Given a filename, return a TreeWidgetItem for a status widget
557 "staged", "deleted, and "untracked" control which icon is used.
560 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
561 icon
= icons
.name_from_basename(icon_name
)
562 return TreeWidgetItem(filename
, icon
, deleted
=deleted
)
565 def add_close_action(widget
):
566 """Adds close action and shortcuts to a widget."""
567 return add_action(widget
, N_('Close...'), widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
571 """Return the current application"""
572 return QtWidgets
.QApplication
.instance()
576 """Return the desktop"""
577 return app().desktop()
582 rect
= desk
.screenGeometry(QtGui
.QCursor().pos())
583 return (rect
.width(), rect
.height())
586 def center_on_screen(widget
):
587 """Move widget to the center of the default screen"""
588 width
, height
= desktop_size()
591 widget
.move(cx
- widget
.width() // 2, cy
- widget
.height() // 2)
594 def default_size(parent
, width
, height
, use_parent_height
=True):
595 """Return the parent's size, or the provided defaults"""
596 if parent
is not None:
597 width
= parent
.width()
598 if use_parent_height
:
599 height
= parent
.height()
600 return (width
, height
)
603 def default_monospace_font():
604 if utils
.is_darwin():
608 mfont
= QtGui
.QFont()
609 mfont
.setFamily(family
)
613 def diff_font_str(context
):
615 font_str
= cfg
.get(prefs
.FONTDIFF
)
617 font_str
= default_monospace_font().toString()
621 def diff_font(context
):
622 return font(diff_font_str(context
))
626 qfont
= QtGui
.QFont()
627 qfont
.fromString(string
)
632 text
='', layout
=None, tooltip
=None, icon
=None, enabled
=True, default
=False
634 """Create a button, set its title, and add it to the parent."""
635 button
= QtWidgets
.QPushButton()
636 button
.setCursor(Qt
.PointingHandCursor
)
637 button
.setFocusPolicy(Qt
.NoFocus
)
639 button
.setText(' ' + text
)
642 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
643 if tooltip
is not None:
644 button
.setToolTip(tooltip
)
645 if layout
is not None:
646 layout
.addWidget(button
)
648 button
.setEnabled(False)
650 button
.setDefault(True)
655 """Create a flat border-less button"""
656 button
= QtWidgets
.QToolButton()
657 button
.setPopupMode(QtWidgets
.QToolButton
.InstantPopup
)
658 button
.setCursor(Qt
.PointingHandCursor
)
659 button
.setFocusPolicy(Qt
.NoFocus
)
661 palette
= QtGui
.QPalette()
662 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
663 highlight_rgb
= rgb_css(highlight
)
665 button
.setStyleSheet(
670 background-color: none;
672 /* Hide the menu indicator */
673 QToolButton::menu-indicator {
677 border: %(border)spx solid %(highlight_rgb)s;
680 % dict(border
=defs
.border
, highlight_rgb
=highlight_rgb
)
685 def create_action_button(tooltip
=None, icon
=None):
686 """Create a small toolbutton for use in dock title widgets"""
687 button
= tool_button()
688 if tooltip
is not None:
689 button
.setToolTip(tooltip
)
692 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
696 def ok_button(text
, default
=True, enabled
=True, icon
=None):
699 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
702 def close_button(text
=None, icon
=None):
703 text
= text
or N_('Close')
704 icon
= icons
.mkicon(icon
, icons
.close
)
705 return create_button(text
=text
, icon
=icon
)
708 def edit_button(enabled
=True, default
=False):
709 return create_button(
710 text
=N_('Edit'), icon
=icons
.edit(), enabled
=enabled
, default
=default
714 def refresh_button(enabled
=True, default
=False):
715 return create_button(
716 text
=N_('Refresh'), icon
=icons
.sync(), enabled
=enabled
, default
=default
720 def checkbox(text
='', tooltip
='', checked
=None):
721 """Create a checkbox"""
722 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
725 def radio(text
='', tooltip
='', checked
=None):
726 """Create a radio button"""
727 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
730 def _checkbox(cls
, text
, tooltip
, checked
):
731 """Create a widget and apply properties"""
736 widget
.setToolTip(tooltip
)
737 if checked
is not None:
738 widget
.setChecked(checked
)
742 class DockTitleBarWidget(QtWidgets
.QFrame
):
743 def __init__(self
, parent
, title
, stretch
=True):
744 QtWidgets
.QFrame
.__init
__(self
, parent
)
745 self
.setAutoFillBackground(True)
746 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
747 qfont
= qlabel
.font()
749 qlabel
.setFont(qfont
)
750 qlabel
.setCursor(Qt
.OpenHandCursor
)
752 self
.close_button
= create_action_button(
753 tooltip
=N_('Close'), icon
=icons
.close()
756 self
.toggle_button
= create_action_button(
757 tooltip
=N_('Detach'), icon
=icons
.external()
760 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
767 self
.main_layout
= hbox(
769 defs
.titlebar_spacing
,
776 self
.setLayout(self
.main_layout
)
778 connect_button(self
.toggle_button
, self
.toggle_floating
)
779 connect_button(self
.close_button
, self
.toggle_visibility
)
781 def toggle_floating(self
):
782 self
.parent().setFloating(not self
.parent().isFloating())
783 self
.update_tooltips()
785 def toggle_visibility(self
):
786 self
.parent().toggleViewAction().trigger()
788 def set_title(self
, title
):
789 self
.label
.setText(title
)
791 def add_corner_widget(self
, widget
):
792 self
.corner_layout
.addWidget(widget
)
794 def update_tooltips(self
):
795 if self
.parent().isFloating():
796 tooltip
= N_('Attach')
798 tooltip
= N_('Detach')
799 self
.toggle_button
.setToolTip(tooltip
)
802 def create_dock(title
, parent
, stretch
=True, widget
=None, fn
=None):
803 """Create a dock widget and set it up accordingly."""
804 dock
= QtWidgets
.QDockWidget(parent
)
805 dock
.setWindowTitle(title
)
806 dock
.setObjectName(title
)
807 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
808 dock
.setTitleBarWidget(titlebar
)
809 dock
.setAutoFillBackground(True)
810 if hasattr(parent
, 'dockwidgets'):
811 parent
.dockwidgets
.append(dock
)
814 assert isinstance(widget
, QtWidgets
.QFrame
), "Docked widget has to be a QFrame"
816 dock
.setWidget(widget
)
820 def hide_dock(widget
):
821 widget
.toggleViewAction().setChecked(False)
825 def create_menu(title
, parent
):
826 """Create a menu and set its title."""
827 qmenu
= DebouncingMenu(title
, parent
)
831 class DebouncingMenu(QtWidgets
.QMenu
):
832 """Menu that debounces mouse release action ie. stops it if occurred
833 right after menu creation.
835 Disables annoying behaviour when RMB is pressed to show menu, cursor is
836 moved accidentally 1px onto newly created menu and released causing to
842 def __init__(self
, title
, parent
):
843 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
844 self
.created_at
= utils
.epoch_millis()
846 def mouseReleaseEvent(self
, event
):
847 threshold
= DebouncingMenu
.threshold_ms
848 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
849 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
852 def add_menu(title
, parent
):
853 """Create a menu and set its title."""
854 menu
= create_menu(title
, parent
)
855 parent
.addAction(menu
.menuAction())
859 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
860 button
= tool_button()
863 button
.setIconSize(QtCore
.QSize(defs
.default_icon
, defs
.default_icon
))
865 button
.setText(' ' + text
)
866 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
867 if tooltip
is not None:
868 button
.setToolTip(tooltip
)
869 if layout
is not None:
870 layout
.addWidget(button
)
874 # pylint: disable=line-too-long
875 def mimedata_from_paths(context
, paths
):
876 """Return mimedata with a list of absolute path URLs
878 The text/x-moz-list format is always included by Qt, and doing
879 mimedata.removeFormat('text/x-moz-url') has no effect.
880 C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
882 gnome-terminal expects utf-16 encoded text, but other terminals,
883 e.g. terminator, prefer utf-8, so allow cola.dragencoding
884 to override the default.
888 abspaths
= [core
.abspath(path
) for path
in paths
]
889 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
891 mimedata
= QtCore
.QMimeData()
892 mimedata
.setUrls(urls
)
894 paths_text
= core
.list2cmdline(abspaths
)
895 encoding
= cfg
.get('cola.dragencoding', 'utf-16')
896 moz_text
= core
.encode(paths_text
, encoding
=encoding
)
897 mimedata
.setData('text/x-moz-url', moz_text
)
902 def path_mimetypes():
903 return ['text/uri-list', 'text/x-moz-url']
906 class BlockSignals(object):
907 """Context manager for blocking a signals on a widget"""
909 def __init__(self
, *widgets
):
910 self
.widgets
= widgets
914 for w
in self
.widgets
:
915 self
.values
[w
] = w
.blockSignals(True)
918 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
919 for w
in self
.widgets
:
920 w
.blockSignals(self
.values
[w
])
923 class Channel(QtCore
.QObject
):
924 finished
= Signal(object)
925 result
= Signal(object)
928 class Task(QtCore
.QRunnable
):
929 """Disable auto-deletion to avoid gc issues
931 Python's garbage collector will try to double-free the task
932 once it's finished, so disable Qt's auto-deletion as a workaround.
936 def __init__(self
, parent
):
937 QtCore
.QRunnable
.__init
__(self
)
939 self
.channel
= Channel(parent
)
941 self
.setAutoDelete(False)
944 self
.result
= self
.task()
945 self
.channel
.result
.emit(self
.result
)
946 self
.channel
.finished
.emit(self
)
948 # pylint: disable=no-self-use
952 def connect(self
, handler
):
953 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
956 class SimpleTask(Task
):
957 """Run a simple callable as a task"""
959 def __init__(self
, parent
, fn
, *args
, **kwargs
):
960 Task
.__init
__(self
, parent
)
967 return self
.fn(*self
.args
, **self
.kwargs
)
970 class RunTask(QtCore
.QObject
):
971 """Runs QRunnable instances and transfers control when they finish"""
973 def __init__(self
, parent
=None):
974 QtCore
.QObject
.__init
__(self
, parent
)
976 self
.task_details
= {}
977 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
978 self
.result_fn
= None
980 def start(self
, task
, progress
=None, finish
=None, result
=None):
981 """Start the task and register a callback"""
982 self
.result_fn
= result
983 if progress
is not None:
985 # prevents garbage collection bugs in certain PyQt4 versions
986 self
.tasks
.append(task
)
988 self
.task_details
[task_id
] = (progress
, finish
, result
)
989 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
990 self
.threadpool
.start(task
)
992 def finish(self
, task
):
995 self
.tasks
.remove(task
)
999 progress
, finish
, result
= self
.task_details
[task_id
]
1000 del self
.task_details
[task_id
]
1002 finish
= progress
= result
= None
1004 if progress
is not None:
1007 if result
is not None:
1010 if finish
is not None:
1014 # Syntax highlighting
1018 color
= QtGui
.QColor()
1019 color
.setRgb(r
, g
, b
)
1023 def rgba(r
, g
, b
, a
=255):
1024 color
= rgb(r
, g
, b
)
1034 """Convert a QColor into an rgb(int, int, int) CSS string"""
1035 return 'rgb(%d, %d, %d)' % (color
.red(), color
.green(), color
.blue())
1039 """Convert a QColor into a hex aabbcc string"""
1040 return '%02x%02x%02x' % (color
.red(), color
.green(), color
.blue())
1043 def hsl(h
, s
, light
):
1044 return QtGui
.QColor
.fromHslF(
1045 utils
.clamp(h
, 0.0, 1.0), utils
.clamp(s
, 0.0, 1.0), utils
.clamp(light
, 0.0, 1.0)
1049 def hsl_css(h
, s
, light
):
1050 return rgb_css(hsl(h
, s
, light
))
1053 def make_format(fg
=None, bg
=None, bold
=False):
1054 fmt
= QtGui
.QTextCharFormat()
1056 fmt
.setForeground(fg
)
1058 fmt
.setBackground(bg
)
1060 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1064 class ImageFormats(object):
1066 # returns a list of QByteArray objects
1067 formats_qba
= QtGui
.QImageReader
.supportedImageFormats()
1068 # portability: python3 data() returns bytes, python2 returns str
1069 decode
= core
.decode
1070 formats
= [decode(x
.data()) for x
in formats_qba
]
1071 self
.extensions
= set(['.' + fmt
for fmt
in formats
])
1073 def ok(self
, filename
):
1074 _
, ext
= os
.path
.splitext(filename
)
1075 return ext
.lower() in self
.extensions