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
):
131 elif isinstance(i
, (QtWidgets
.QHBoxLayout
, QtWidgets
.QVBoxLayout
,
132 QtWidgets
.QFormLayout
, QtWidgets
.QLayout
)):
138 elif isinstance(i
, int_types
):
144 def form(margin
, spacing
, *widgets
):
145 """Create a QFormLayout with the specified sizes and items"""
146 layout
= QtWidgets
.QFormLayout()
147 layout
.setSpacing(spacing
)
148 layout
.setFieldGrowthPolicy(QtWidgets
.QFormLayout
.ExpandingFieldsGrow
)
149 set_margin(layout
, margin
)
151 for idx
, (name
, widget
) in enumerate(widgets
):
152 if isinstance(name
, (str, ustr
)):
153 layout
.addRow(name
, widget
)
155 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.LabelRole
, name
)
156 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.FieldRole
, widget
)
161 def grid(margin
, spacing
, *widgets
):
162 """Create a QGridLayout with the specified sizes and items"""
163 layout
= QtWidgets
.QGridLayout()
164 layout
.setSpacing(spacing
)
165 set_margin(layout
, margin
)
169 if isinstance(item
, QtWidgets
.QWidget
):
170 layout
.addWidget(*row
)
171 elif isinstance(item
, QtWidgets
.QLayoutItem
):
177 def splitter(orientation
, *widgets
):
178 """Create a spliter over the specified widgets
180 :param orientation: Qt.Horizontal or Qt.Vertical
183 layout
= QtWidgets
.QSplitter()
184 layout
.setOrientation(orientation
)
185 layout
.setHandleWidth(defs
.handle_width
)
186 layout
.setChildrenCollapsible(True)
188 for idx
, widget
in enumerate(widgets
):
189 layout
.addWidget(widget
)
190 layout
.setStretchFactor(idx
, 1)
192 # Workaround for Qt not setting the WA_Hover property for QSplitter
193 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
194 layout
.handle(1).setAttribute(Qt
.WA_Hover
)
199 def label(text
=None, align
=None, fmt
=None, selectable
=True):
200 """Create a QLabel with the specified properties"""
201 widget
= QtWidgets
.QLabel()
202 if align
is not None:
203 widget
.setAlignment(align
)
205 widget
.setTextFormat(fmt
)
207 widget
.setTextInteractionFlags(Qt
.TextBrowserInteraction
)
208 widget
.setOpenExternalLinks(True)
214 class ComboBox(QtWidgets
.QComboBox
):
215 """Custom read-only combobox with a convenient API"""
217 def __init__(self
, items
=None, editable
=False, parent
=None):
218 super(ComboBox
, self
).__init
__(parent
)
219 self
.setEditable(editable
)
223 def set_index(self
, idx
):
224 idx
= utils
.clamp(idx
, 0, self
.count()-1)
225 self
.setCurrentIndex(idx
)
228 def combo(items
, editable
=False, parent
=None):
229 """Create a readonly (by default) combobox from a list of items"""
230 return ComboBox(editable
=editable
, items
=items
, parent
=parent
)
233 def textbrowser(text
=None):
234 """Create a QTextBrowser for the specified text"""
235 widget
= QtWidgets
.QTextBrowser()
236 widget
.setOpenExternalLinks(True)
242 def add_completer(widget
, items
):
243 """Add simple completion to a widget"""
244 completer
= QtWidgets
.QCompleter(items
, widget
)
245 completer
.setCaseSensitivity(Qt
.CaseInsensitive
)
246 completer
.setCompletionMode(QtWidgets
.QCompleter
.InlineCompletion
)
247 widget
.setCompleter(completer
)
250 def prompt(msg
, title
=None, text
='', parent
=None):
251 """Presents the user with an input widget and returns the input."""
255 parent
= active_window()
256 result
= QtWidgets
.QInputDialog
.getText(
257 parent
, title
, msg
, QtWidgets
.QLineEdit
.Normal
, text
)
258 return (result
[0], result
[1])
261 def prompt_n(msg
, inputs
):
262 """Presents the user with N input widgets and returns the results"""
263 dialog
= QtWidgets
.QDialog(active_window())
264 dialog
.setWindowModality(Qt
.WindowModal
)
265 dialog
.setWindowTitle(msg
)
269 if len(k
+ v
) > len(long_value
):
272 metrics
= QtGui
.QFontMetrics(dialog
.font())
273 min_width
= metrics
.width(long_value
) + 100
276 dialog
.setMinimumWidth(min_width
)
278 ok_b
= ok_button(msg
, enabled
=False)
279 close_b
= close_button()
284 return [pair
[1].text().strip() for pair
in form_widgets
]
286 for name
, value
in inputs
:
287 lineedit
= QtWidgets
.QLineEdit()
288 # Enable the OK button only when all fields have been populated
289 lineedit
.textChanged
.connect(
290 lambda x
: ok_b
.setEnabled(all(get_values())))
292 lineedit
.setText(value
)
293 form_widgets
.append((name
, lineedit
))
296 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
297 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
,
298 STRETCH
, close_b
, ok_b
)
299 main_layout
= vbox(defs
.margin
, defs
.button_spacing
,
300 form_layout
, button_layout
)
301 dialog
.setLayout(main_layout
)
304 connect_button(ok_b
, dialog
.accept
)
305 connect_button(close_b
, dialog
.reject
)
307 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
309 ok
= accepted
and all(text
)
313 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
315 TYPE
= QtGui
.QStandardItem
.UserType
+ 101
317 def __init__(self
, path
, icon
, deleted
):
318 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
320 self
.deleted
= deleted
321 self
.setIcon(0, icons
.from_name(icon
))
322 self
.setText(0, path
)
328 def paths_from_indexes(model
, indexes
,
329 item_type
=TreeWidgetItem
.TYPE
,
331 """Return paths from a list of QStandardItemModel indexes"""
332 items
= [model
.itemFromIndex(i
) for i
in indexes
]
333 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
336 def _true_filter(_x
):
340 def paths_from_items(items
,
341 item_type
=TreeWidgetItem
.TYPE
,
343 """Return a list of paths from a list of items"""
344 if item_filter
is None:
345 item_filter
= _true_filter
346 return [i
.path
for i
in items
347 if i
.type() == item_type
and item_filter(i
)]
350 def tree_selection(tree_item
, items
):
351 """Returns an array of model items that correspond to the selected
352 QTreeWidgetItem children"""
354 count
= min(tree_item
.childCount(), len(items
))
355 for idx
in range(count
):
356 if tree_item
.child(idx
).isSelected():
357 selected
.append(items
[idx
])
362 def tree_selection_items(tree_item
):
363 """Returns selected widget items"""
365 for idx
in range(tree_item
.childCount()):
366 child
= tree_item
.child(idx
)
367 if child
.isSelected():
368 selected
.append(child
)
373 def selected_item(list_widget
, items
):
374 """Returns the model item that corresponds to the selected QListWidget
376 widget_items
= list_widget
.selectedItems()
379 widget_item
= widget_items
[0]
380 row
= list_widget
.row(widget_item
)
388 def selected_items(list_widget
, items
):
389 """Returns an array of model items that correspond to the selected
391 item_count
= len(items
)
393 for widget_item
in list_widget
.selectedItems():
394 row
= list_widget
.row(widget_item
)
396 selected
.append(items
[row
])
400 def open_file(title
, directory
=None):
401 """Creates an Open File dialog and returns a filename."""
402 result
= compat
.getopenfilename(parent
=active_window(),
408 def open_files(title
, directory
=None, filters
=''):
409 """Creates an Open File dialog and returns a list of filenames."""
410 result
= compat
.getopenfilenames(parent
=active_window(),
417 def opendir_dialog(caption
, path
):
418 """Prompts for a directory path"""
420 options
= (QtWidgets
.QFileDialog
.ShowDirsOnly |
421 QtWidgets
.QFileDialog
.DontResolveSymlinks
)
422 return compat
.getexistingdirectory(parent
=active_window(),
428 def save_as(filename
, title
='Save As...'):
429 """Creates a Save File dialog and returns a filename."""
430 result
= compat
.getsavefilename(parent
=active_window(),
436 def copy_path(filename
, absolute
=True):
437 """Copy a filename to the clipboard"""
441 filename
= core
.abspath(filename
)
442 set_clipboard(filename
)
445 def set_clipboard(text
):
446 """Sets the copy/paste buffer to text."""
449 clipboard
= QtWidgets
.QApplication
.clipboard()
450 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
451 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
455 # pylint: disable=line-too-long
456 def persist_clipboard():
457 """Persist the clipboard
459 X11 stores only a reference to the clipboard data.
460 Send a clipboard event to force a copy of the clipboard to occur.
461 This ensures that the clipboard is present after git-cola exits.
462 Otherwise, the reference is destroyed on exit.
464 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
467 clipboard
= QtWidgets
.QApplication
.clipboard()
468 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
469 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
472 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
474 action
= _add_action(widget
, text
, tip
, fn
,
475 connect_action_bool
, *shortcuts
)
476 action
.setCheckable(True)
477 action
.setChecked(checked
)
481 def add_action(widget
, text
, fn
, *shortcuts
):
483 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
486 def add_action_with_status_tip(widget
, text
, tip
, fn
, *shortcuts
):
487 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
490 def _add_action(widget
, text
, tip
, fn
, connect
, *shortcuts
):
491 action
= QtWidgets
.QAction(text
, widget
)
492 if hasattr(action
, 'setIconVisibleInMenu'):
493 action
.setIconVisibleInMenu(True)
495 action
.setStatusTip(tip
)
498 action
.setShortcuts(shortcuts
)
499 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
500 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
501 widget
.addAction(action
)
505 def set_selected_item(widget
, idx
):
506 """Sets a the currently selected item to the item at index idx."""
507 if isinstance(widget
, QtWidgets
.QTreeWidget
):
508 item
= widget
.topLevelItem(idx
)
510 item
.setSelected(True)
511 widget
.setCurrentItem(item
)
514 def add_items(widget
, items
):
515 """Adds items to a widget."""
522 def set_items(widget
, items
):
523 """Clear the existing widget contents and set the new items."""
525 add_items(widget
, items
)
528 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
529 """Given a filename, return a TreeWidgetItem for a status widget
531 "staged", "deleted, and "untracked" control which icon is used.
534 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
535 return TreeWidgetItem(filename
, icons
.name_from_basename(icon_name
),
539 def add_close_action(widget
):
540 """Adds close action and shortcuts to a widget."""
541 return add_action(widget
, N_('Close...'),
542 widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
546 """Return the current application"""
547 return QtWidgets
.QApplication
.instance()
551 """Return the desktop"""
552 return app().desktop()
557 rect
= desk
.screenGeometry(QtGui
.QCursor().pos())
558 return (rect
.width(), rect
.height())
561 def center_on_screen(widget
):
562 """Move widget to the center of the default screen"""
563 width
, height
= desktop_size()
566 widget
.move(cx
- widget
.width()//2, cy
- widget
.height()//2)
569 def default_size(parent
, width
, height
, use_parent_height
=True):
570 """Return the parent's size, or the provided defaults"""
571 if parent
is not None:
572 width
= parent
.width()
573 if use_parent_height
:
574 height
= parent
.height()
575 return (width
, height
)
578 def default_monospace_font():
579 if utils
.is_darwin():
583 mfont
= QtGui
.QFont()
584 mfont
.setFamily(family
)
588 def diff_font_str(context
):
590 font_str
= cfg
.get(prefs
.FONTDIFF
)
592 font_str
= default_monospace_font().toString()
596 def diff_font(context
):
597 return font(diff_font_str(context
))
601 qfont
= QtGui
.QFont()
602 qfont
.fromString(string
)
606 def create_button(text
='', layout
=None, tooltip
=None, icon
=None,
607 enabled
=True, default
=False):
608 """Create a button, set its title, and add it to the parent."""
609 button
= QtWidgets
.QPushButton()
610 button
.setCursor(Qt
.PointingHandCursor
)
611 button
.setFocusPolicy(Qt
.NoFocus
)
613 button
.setText(' ' + text
)
616 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
617 if tooltip
is not None:
618 button
.setToolTip(tooltip
)
619 if layout
is not None:
620 layout
.addWidget(button
)
622 button
.setEnabled(False)
624 button
.setDefault(True)
628 def create_action_button(tooltip
=None, icon
=None):
629 button
= QtWidgets
.QPushButton()
630 button
.setCursor(Qt
.PointingHandCursor
)
631 button
.setFocusPolicy(Qt
.NoFocus
)
633 if tooltip
is not None:
634 button
.setToolTip(tooltip
)
637 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
641 def ok_button(text
, default
=True, enabled
=True, icon
=None):
644 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
647 def close_button(text
=None, icon
=None):
648 text
= text
or N_('Close')
649 icon
= icons
.mkicon(icon
, icons
.close
)
650 return create_button(text
=text
, icon
=icon
)
653 def edit_button(enabled
=True, default
=False):
654 return create_button(text
=N_('Edit'), icon
=icons
.edit(),
655 enabled
=enabled
, default
=default
)
658 def refresh_button(enabled
=True, default
=False):
659 return create_button(text
=N_('Refresh'), icon
=icons
.sync(),
660 enabled
=enabled
, default
=default
)
663 def hide_button_menu_indicator(button
):
664 """Hide the menu indicator icon on buttons"""
666 name
= button
.__class
__.__name
__
668 %(name)s::menu-indicator {
672 if name
== 'QPushButton':
678 button
.setStyleSheet(stylesheet
% dict(name
=name
))
681 def checkbox(text
='', tooltip
='', checked
=None):
682 """Create a checkbox"""
683 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
686 def radio(text
='', tooltip
='', checked
=None):
687 """Create a radio button"""
688 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
691 def _checkbox(cls
, text
, tooltip
, checked
):
692 """Create a widget and apply properties"""
697 widget
.setToolTip(tooltip
)
698 if checked
is not None:
699 widget
.setChecked(checked
)
703 class DockTitleBarWidget(QtWidgets
.QWidget
):
705 def __init__(self
, parent
, title
, stretch
=True):
706 QtWidgets
.QWidget
.__init
__(self
, parent
)
707 self
.setAutoFillBackground(True)
708 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
709 qfont
= qlabel
.font()
711 qlabel
.setFont(qfont
)
712 qlabel
.setCursor(Qt
.OpenHandCursor
)
714 self
.close_button
= create_action_button(
715 tooltip
=N_('Close'), icon
=icons
.close())
717 self
.toggle_button
= create_action_button(
718 tooltip
=N_('Detach'), icon
=icons
.external())
720 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
727 self
.main_layout
= hbox(defs
.small_margin
, defs
.spacing
,
728 qlabel
, separator
, self
.corner_layout
,
729 self
.toggle_button
, self
.close_button
)
730 self
.setLayout(self
.main_layout
)
732 connect_button(self
.toggle_button
, self
.toggle_floating
)
733 connect_button(self
.close_button
, self
.toggle_visibility
)
735 def toggle_floating(self
):
736 self
.parent().setFloating(not self
.parent().isFloating())
737 self
.update_tooltips()
739 def toggle_visibility(self
):
740 self
.parent().toggleViewAction().trigger()
742 def set_title(self
, title
):
743 self
.label
.setText(title
)
745 def add_corner_widget(self
, widget
):
746 self
.corner_layout
.addWidget(widget
)
748 def update_tooltips(self
):
749 if self
.parent().isFloating():
750 tooltip
= N_('Attach')
752 tooltip
= N_('Detach')
753 self
.toggle_button
.setToolTip(tooltip
)
756 def create_dock(title
, parent
, stretch
=True, widget
=None, fn
=None):
757 """Create a dock widget and set it up accordingly."""
758 dock
= QtWidgets
.QDockWidget(parent
)
759 dock
.setWindowTitle(title
)
760 dock
.setObjectName(title
)
761 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
762 dock
.setTitleBarWidget(titlebar
)
763 dock
.setAutoFillBackground(True)
764 if hasattr(parent
, 'dockwidgets'):
765 parent
.dockwidgets
.append(dock
)
769 dock
.setWidget(widget
)
773 def hide_dock(widget
):
774 widget
.toggleViewAction().setChecked(False)
778 def create_menu(title
, parent
):
779 """Create a menu and set its title."""
780 qmenu
= DebouncingMenu(title
, parent
)
784 class DebouncingMenu(QtWidgets
.QMenu
):
785 """Menu that debounces mouse release action ie. stops it if occurred
786 right after menu creation.
788 Disables annoying behaviour when RMB is pressed to show menu, cursor is
789 moved accidentally 1px onto newly created menu and released causing to
795 def __init__(self
, title
, parent
):
796 QtWidgets
.QMenu
.__init
__(self
, title
, parent
)
797 self
.created_at
= utils
.epoch_millis()
799 def mouseReleaseEvent(self
, event
):
800 threshold
= DebouncingMenu
.threshold_ms
801 if (utils
.epoch_millis() - self
.created_at
) > threshold
:
802 QtWidgets
.QMenu
.mouseReleaseEvent(self
, event
)
805 def add_menu(title
, parent
):
806 """Create a menu and set its title."""
807 menu
= create_menu(title
, parent
)
808 parent
.addAction(menu
.menuAction())
812 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
813 button
= QtWidgets
.QToolButton()
814 button
.setAutoRaise(True)
815 button
.setAutoFillBackground(True)
816 button
.setCursor(Qt
.PointingHandCursor
)
817 button
.setFocusPolicy(Qt
.NoFocus
)
820 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
822 button
.setText(' ' + text
)
823 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
824 if tooltip
is not None:
825 button
.setToolTip(tooltip
)
826 if layout
is not None:
827 layout
.addWidget(button
)
831 # pylint: disable=line-too-long
832 def mimedata_from_paths(context
, paths
):
833 """Return mimedata with a list of absolute path URLs
835 The text/x-moz-list format is always included by Qt, and doing
836 mimedata.removeFormat('text/x-moz-url') has no effect.
837 C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
839 gnome-terminal expects utf-16 encoded text, but other terminals,
840 e.g. terminator, prefer utf-8, so allow cola.dragencoding
841 to override the default.
845 abspaths
= [core
.abspath(path
) for path
in paths
]
846 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
848 mimedata
= QtCore
.QMimeData()
849 mimedata
.setUrls(urls
)
851 paths_text
= core
.list2cmdline(abspaths
)
852 encoding
= cfg
.get('cola.dragencoding', 'utf-16')
853 moz_text
= core
.encode(paths_text
, encoding
=encoding
)
854 mimedata
.setData('text/x-moz-url', moz_text
)
859 def path_mimetypes():
860 return ['text/uri-list', 'text/x-moz-url']
863 class BlockSignals(object):
864 """Context manager for blocking a signals on a widget"""
866 def __init__(self
, *widgets
):
867 self
.widgets
= widgets
871 for w
in self
.widgets
:
872 self
.values
[w
] = w
.blockSignals(True)
875 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
876 for w
in self
.widgets
:
877 w
.blockSignals(self
.values
[w
])
880 class Channel(QtCore
.QObject
):
881 finished
= Signal(object)
882 result
= Signal(object)
885 class Task(QtCore
.QRunnable
):
886 """Disable auto-deletion to avoid gc issues
888 Python's garbage collector will try to double-free the task
889 once it's finished, so disable Qt's auto-deletion as a workaround.
893 def __init__(self
, parent
):
894 QtCore
.QRunnable
.__init
__(self
)
896 self
.channel
= Channel(parent
)
898 self
.setAutoDelete(False)
901 self
.result
= self
.task()
902 self
.channel
.result
.emit(self
.result
)
903 self
.channel
.finished
.emit(self
)
908 def connect(self
, handler
):
909 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
912 class SimpleTask(Task
):
913 """Run a simple callable as a task"""
915 def __init__(self
, parent
, fn
, *args
, **kwargs
):
916 Task
.__init
__(self
, parent
)
923 return self
.fn(*self
.args
, **self
.kwargs
)
926 class RunTask(QtCore
.QObject
):
927 """Runs QRunnable instances and transfers control when they finish"""
929 def __init__(self
, parent
=None):
930 QtCore
.QObject
.__init
__(self
, parent
)
932 self
.task_details
= {}
933 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
934 self
.result_fn
= None
936 def start(self
, task
, progress
=None, finish
=None, result
=None):
937 """Start the task and register a callback"""
938 self
.result_fn
= result
939 if progress
is not None:
941 # prevents garbage collection bugs in certain PyQt4 versions
942 self
.tasks
.append(task
)
944 self
.task_details
[task_id
] = (progress
, finish
, result
)
945 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
946 self
.threadpool
.start(task
)
948 def finish(self
, task
):
951 self
.tasks
.remove(task
)
955 progress
, finish
, result
= self
.task_details
[task_id
]
956 del self
.task_details
[task_id
]
958 finish
= progress
= result
= None
960 if progress
is not None:
963 if result
is not None:
966 if finish
is not None:
970 # Syntax highlighting
973 color
= QtGui
.QColor()
974 color
.setRgb(r
, g
, b
)
978 def rgba(r
, g
, b
, a
=255):
989 """Convert a QColor into an rgb(int, int, int) CSS string"""
990 return 'rgb(%d, %d, %d)' % (color
.red(), color
.green(), color
.blue())
994 """Convert a QColor into a hex aabbcc string"""
995 return '%02x%02x%02x' % (color
.red(), color
.green(), color
.blue())
998 def make_format(fg
=None, bg
=None, bold
=False):
999 fmt
= QtGui
.QTextCharFormat()
1001 fmt
.setForeground(fg
)
1003 fmt
.setBackground(bg
)
1005 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1009 class ImageFormats(object):
1012 # returns a list of QByteArray objects
1013 formats_qba
= QtGui
.QImageReader
.supportedImageFormats()
1014 # portability: python3 data() returns bytes, python2 returns str
1015 decode
= core
.decode
1016 formats
= [decode(x
.data()) for x
in formats_qba
]
1017 self
.extensions
= set(['.' + fmt
for fmt
in formats
])
1019 def ok(self
, filename
):
1020 _
, ext
= os
.path
.splitext(filename
)
1021 return ext
.lower() in self
.extensions