1 # Copyright (c) 2008-2016 David Aguilar
2 """Miscellaneous Qt utility functions."""
3 from __future__
import division
, absolute_import
, unicode_literals
5 from qtpy
import compat
7 from qtpy
import QtCore
8 from qtpy
import QtWidgets
9 from qtpy
.QtCore
import Qt
10 from qtpy
.QtCore
import Signal
18 from .interaction
import Interaction
19 from .compat
import int_types
20 from .compat
import ustr
21 from .models
import prefs
22 from .widgets
import defs
29 def connect_action(action
, fn
):
30 """Connect an action to a function"""
31 action
.triggered
[bool].connect(lambda x
: fn())
34 def connect_action_bool(action
, fn
):
35 """Connect a triggered(bool) action to a function"""
36 action
.triggered
[bool].connect(fn
)
39 def connect_button(button
, fn
):
40 """Connect a button to a function"""
41 button
.pressed
.connect(fn
)
44 def button_action(button
, action
):
45 """Make a button trigger an action"""
46 connect_button(button
, action
.trigger
)
49 def connect_toggle(toggle
, fn
):
50 """Connect a toggle button to a function"""
51 toggle
.toggled
.connect(fn
)
55 """Return the active window for the current application"""
56 return QtWidgets
.QApplication
.activeWindow()
59 def hbox(margin
, spacing
, *items
):
60 """Create an HBoxLayout with the specified sizes and items"""
61 return box(QtWidgets
.QHBoxLayout
, margin
, spacing
, *items
)
64 def vbox(margin
, spacing
, *items
):
65 """Create a VBoxLayout with the specified sizes and items"""
66 return box(QtWidgets
.QVBoxLayout
, margin
, spacing
, *items
)
69 def buttongroup(*items
):
70 """Create a QButtonGroup for the specified items"""
71 group
= QtWidgets
.QButtonGroup()
77 def set_margin(layout
, margin
):
78 """Set the content margins for a layout"""
79 layout
.setContentsMargins(margin
, margin
, margin
, margin
)
82 def box(cls
, margin
, spacing
, *items
):
83 """Create a QBoxLayout with the specified sizes and items"""
87 layout
.setSpacing(spacing
)
88 set_margin(layout
, margin
)
91 if isinstance(i
, QtWidgets
.QWidget
):
93 elif isinstance(i
, (QtWidgets
.QHBoxLayout
, QtWidgets
.QVBoxLayout
,
94 QtWidgets
.QFormLayout
, QtWidgets
.QLayout
)):
100 elif isinstance(i
, int_types
):
106 def form(margin
, spacing
, *widgets
):
107 """Create a QFormLayout with the specified sizes and items"""
108 layout
= QtWidgets
.QFormLayout()
109 layout
.setSpacing(spacing
)
110 layout
.setFieldGrowthPolicy(QtWidgets
.QFormLayout
.ExpandingFieldsGrow
)
111 set_margin(layout
, margin
)
113 for idx
, (name
, widget
) in enumerate(widgets
):
114 if isinstance(name
, (str, ustr
)):
115 layout
.addRow(name
, widget
)
117 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.LabelRole
, name
)
118 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.FieldRole
, widget
)
123 def grid(margin
, spacing
, *widgets
):
124 """Create a QGridLayout with the specified sizes and items"""
125 layout
= QtWidgets
.QGridLayout()
126 layout
.setSpacing(spacing
)
127 set_margin(layout
, margin
)
131 if isinstance(item
, QtWidgets
.QWidget
):
132 layout
.addWidget(*row
)
133 elif isinstance(item
, QtWidgets
.QLayoutItem
):
139 def splitter(orientation
, *widgets
):
140 """Create a spliter over the specified widgets
142 :param orientation: Qt.Horizontal or Qt.Vertical
145 layout
= QtWidgets
.QSplitter()
146 layout
.setOrientation(orientation
)
147 layout
.setHandleWidth(defs
.handle_width
)
148 layout
.setChildrenCollapsible(True)
149 for idx
, widget
in enumerate(widgets
):
150 layout
.addWidget(widget
)
151 layout
.setStretchFactor(idx
, 1)
156 def label(text
=None, align
=None, fmt
=None, selectable
=True, stylesheet
=None):
157 """Create a QLabel with the specified properties"""
158 widget
= QtWidgets
.QLabel()
160 widget
.setStyleSheet(stylesheet
)
161 if align
is not None:
162 widget
.setAlignment(align
)
164 widget
.setTextFormat(fmt
)
166 widget
.setTextInteractionFlags(Qt
.TextBrowserInteraction
)
167 widget
.setOpenExternalLinks(True)
173 def textbrowser(text
=None):
174 """Create a QTextBrowser for the specified text"""
175 widget
= QtWidgets
.QTextBrowser()
176 widget
.setOpenExternalLinks(True)
182 def prompt(msg
, title
=None, text
=''):
183 """Presents the user with an input widget and returns the input."""
186 result
= QtWidgets
.QInputDialog
.getText(
187 active_window(), msg
, title
,
188 QtWidgets
.QLineEdit
.Normal
, text
)
189 return (result
[0], result
[1])
192 def prompt_n(msg
, inputs
):
193 """Presents the user with N input widgets and returns the results"""
194 dialog
= QtWidgets
.QDialog(active_window())
195 dialog
.setWindowModality(Qt
.WindowModal
)
196 dialog
.setWindowTitle(msg
)
200 if len(k
+ v
) > len(long_value
):
203 metrics
= QtGui
.QFontMetrics(dialog
.font())
204 min_width
= metrics
.width(long_value
) + 100
207 dialog
.setMinimumWidth(min_width
)
209 ok_b
= ok_button(msg
, enabled
=False)
210 close_b
= close_button()
215 return [pair
[1].text().strip() for pair
in form_widgets
]
217 for name
, value
in inputs
:
218 lineedit
= QtWidgets
.QLineEdit()
219 # Enable the OK button only when all fields have been populated
220 lineedit
.textChanged
.connect(
221 lambda x
: ok_b
.setEnabled(all(get_values())))
223 lineedit
.setText(value
)
224 form_widgets
.append((name
, lineedit
))
227 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
228 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
,
229 STRETCH
, close_b
, ok_b
)
230 main_layout
= vbox(defs
.margin
, defs
.button_spacing
,
231 form_layout
, button_layout
)
232 dialog
.setLayout(main_layout
)
235 connect_button(ok_b
, dialog
.accept
)
236 connect_button(close_b
, dialog
.reject
)
238 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
240 ok
= accepted
and all(text
)
244 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
246 TYPE
= QtGui
.QStandardItem
.UserType
+ 101
248 def __init__(self
, path
, icon
, deleted
):
249 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
251 self
.deleted
= deleted
252 self
.setIcon(0, icons
.from_name(icon
))
253 self
.setText(0, path
)
259 def paths_from_indexes(model
, indexes
,
260 item_type
=TreeWidgetItem
.TYPE
,
262 """Return paths from a list of QStandardItemModel indexes"""
263 items
= [model
.itemFromIndex(i
) for i
in indexes
]
264 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
271 def paths_from_items(items
,
272 item_type
=TreeWidgetItem
.TYPE
,
274 """Return a list of paths from a list of items"""
275 if item_filter
is None:
276 item_filter
= _true_filter
277 return [i
.path
for i
in items
278 if i
.type() == item_type
and item_filter(i
)]
281 def confirm(title
, text
, informative_text
, ok_text
,
282 icon
=None, default
=True,
283 cancel_text
=None, cancel_icon
=None):
284 """Confirm that an action should take place"""
285 msgbox
= QtWidgets
.QMessageBox(active_window())
286 msgbox
.setWindowModality(Qt
.WindowModal
)
287 msgbox
.setWindowTitle(title
)
289 msgbox
.setInformativeText(informative_text
)
291 icon
= icons
.mkicon(icon
, icons
.ok
)
292 ok
= msgbox
.addButton(ok_text
, QtWidgets
.QMessageBox
.ActionRole
)
295 cancel
= msgbox
.addButton(QtWidgets
.QMessageBox
.Cancel
)
296 cancel_icon
= icons
.mkicon(cancel_icon
, icons
.close
)
297 cancel
.setIcon(cancel_icon
)
299 cancel
.setText(cancel_text
)
302 msgbox
.setDefaultButton(ok
)
304 msgbox
.setDefaultButton(cancel
)
306 return msgbox
.clickedButton() == ok
309 class ResizeableMessageBox(QtWidgets
.QMessageBox
):
311 def __init__(self
, parent
):
312 QtWidgets
.QMessageBox
.__init
__(self
, parent
)
313 self
.setMouseTracking(True)
314 self
.setSizeGripEnabled(True)
316 def event(self
, event
):
317 res
= QtWidgets
.QMessageBox
.event(self
, event
)
318 event_type
= event
.type()
319 if (event_type
== QtCore
.QEvent
.MouseMove
or
320 event_type
== QtCore
.QEvent
.MouseButtonPress
):
321 maxi
= QtCore
.QSize(defs
.max_size
, defs
.max_size
)
322 self
.setMaximumSize(maxi
)
323 text
= self
.findChild(QtWidgets
.QTextEdit
)
325 expand
= QtWidgets
.QSizePolicy
.Expanding
326 text
.setSizePolicy(QtWidgets
.QSizePolicy(expand
, expand
))
327 text
.setMaximumSize(maxi
)
331 def critical(title
, message
=None, details
=None):
332 """Show a warning with the provided title and message."""
335 mbox
= ResizeableMessageBox(active_window())
336 mbox
.setWindowTitle(title
)
337 mbox
.setTextFormat(Qt
.PlainText
)
338 mbox
.setText(message
)
339 mbox
.setIcon(QtWidgets
.QMessageBox
.Critical
)
340 mbox
.setStandardButtons(QtWidgets
.QMessageBox
.Close
)
341 mbox
.setDefaultButton(QtWidgets
.QMessageBox
.Close
)
343 mbox
.setDetailedText(details
)
347 def information(title
, message
=None, details
=None, informative_text
=None):
348 """Show information with the provided title and message."""
351 mbox
= QtWidgets
.QMessageBox(active_window())
352 mbox
.setStandardButtons(QtWidgets
.QMessageBox
.Close
)
353 mbox
.setDefaultButton(QtWidgets
.QMessageBox
.Close
)
354 mbox
.setWindowTitle(title
)
355 mbox
.setWindowModality(Qt
.WindowModal
)
356 mbox
.setTextFormat(Qt
.PlainText
)
357 mbox
.setText(message
)
359 mbox
.setInformativeText(informative_text
)
361 mbox
.setDetailedText(details
)
362 # Render into a 1-inch wide pixmap
363 pixmap
= icons
.cola().pixmap(defs
.large_icon
)
364 mbox
.setIconPixmap(pixmap
)
368 def question(title
, msg
, default
=True):
369 """Launches a QMessageBox question with the provided title and message.
370 Passing "default=False" will make "No" the default choice."""
371 yes
= QtWidgets
.QMessageBox
.Yes
372 no
= QtWidgets
.QMessageBox
.No
379 parent
= active_window()
380 MessageBox
= QtWidgets
.QMessageBox
381 result
= MessageBox
.question(parent
, title
, msg
, buttons
, default
)
382 return result
== QtWidgets
.QMessageBox
.Yes
385 def tree_selection(tree_item
, items
):
386 """Returns an array of model items that correspond to the selected
387 QTreeWidgetItem children"""
389 count
= min(tree_item
.childCount(), len(items
))
390 for idx
in range(count
):
391 if tree_item
.child(idx
).isSelected():
392 selected
.append(items
[idx
])
397 def tree_selection_items(tree_item
):
398 """Returns selected widget items"""
400 for idx
in range(tree_item
.childCount()):
401 child
= tree_item
.child(idx
)
402 if child
.isSelected():
403 selected
.append(child
)
408 def selected_item(list_widget
, items
):
409 """Returns the model item that corresponds to the selected QListWidget
411 widget_items
= list_widget
.selectedItems()
414 widget_item
= widget_items
[0]
415 row
= list_widget
.row(widget_item
)
422 def selected_items(list_widget
, items
):
423 """Returns an array of model items that correspond to the selected
425 item_count
= len(items
)
427 for widget_item
in list_widget
.selectedItems():
428 row
= list_widget
.row(widget_item
)
430 selected
.append(items
[row
])
434 def open_file(title
, directory
=None):
435 """Creates an Open File dialog and returns a filename."""
436 result
= compat
.getopenfilename(parent
=active_window(),
442 def open_files(title
, directory
=None, filters
=''):
443 """Creates an Open File dialog and returns a list of filenames."""
444 result
= compat
.getopenfilenames(parent
=active_window(),
451 def opendir_dialog(caption
, path
):
452 """Prompts for a directory path"""
454 options
= (QtWidgets
.QFileDialog
.ShowDirsOnly |
455 QtWidgets
.QFileDialog
.DontResolveSymlinks
)
456 return compat
.getexistingdirectory(parent
=active_window(),
462 def save_as(filename
, title
='Save As...'):
463 """Creates a Save File dialog and returns a filename."""
464 result
= compat
.getsavefilename(parent
=active_window(),
470 def copy_path(filename
, absolute
=True):
471 """Copy a filename to the clipboard"""
475 filename
= core
.abspath(filename
)
476 set_clipboard(filename
)
479 def set_clipboard(text
):
480 """Sets the copy/paste buffer to text."""
483 clipboard
= QtWidgets
.QApplication
.clipboard()
484 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
485 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
489 def persist_clipboard():
490 """Persist the clipboard
492 X11 stores only a reference to the clipboard data.
493 Send a clipboard event to force a copy of the clipboard to occur.
494 This ensures that the clipboard is present after git-cola exits.
495 Otherwise, the reference is destroyed on exit.
497 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
500 clipboard
= QtWidgets
.QApplication
.clipboard()
501 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
502 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
505 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
507 action
= _add_action(widget
, text
, tip
, fn
, connect_action_bool
, *shortcuts
)
508 action
.setCheckable(True)
509 action
.setChecked(checked
)
513 def add_action(widget
, text
, fn
, *shortcuts
):
515 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
518 def add_action_with_status_tip(widget
, text
, tip
, fn
, *shortcuts
):
519 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
522 def _add_action(widget
, text
, tip
, fn
, connect
, *shortcuts
):
523 action
= QtWidgets
.QAction(text
, widget
)
524 if hasattr(action
, 'setIconVisibleInMenu'):
525 action
.setIconVisibleInMenu(True)
527 action
.setStatusTip(tip
)
530 action
.setShortcuts(shortcuts
)
531 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
532 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
533 widget
.addAction(action
)
537 def set_selected_item(widget
, idx
):
538 """Sets a the currently selected item to the item at index idx."""
539 if type(widget
) is QtWidgets
.QTreeWidget
:
540 item
= widget
.topLevelItem(idx
)
542 item
.setSelected(True)
543 widget
.setCurrentItem(item
)
546 def add_items(widget
, items
):
547 """Adds items to a widget."""
554 def set_items(widget
, items
):
555 """Clear the existing widget contents and set the new items."""
557 add_items(widget
, items
)
560 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
561 """Given a filename, return a TreeWidgetItem for a status widget
563 "staged", "deleted, and "untracked" control which icon is used.
566 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
567 return TreeWidgetItem(filename
, icons
.name_from_basename(icon_name
),
571 def add_close_action(widget
):
572 """Adds close action and shortcuts to a widget."""
573 return add_action(widget
, N_('Close...'),
574 widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
578 return QtWidgets
.QApplication
.instance().desktop()
581 def center_on_screen(widget
):
582 """Move widget to the center of the default screen"""
584 rect
= desk
.screenGeometry(QtGui
.QCursor().pos())
585 cy
= rect
.height()//2
587 widget
.move(cx
- widget
.width()//2, cy
- widget
.height()//2)
590 def default_size(parent
, width
, height
):
591 """Return the parent's size, or the provided defaults"""
592 if parent
is not None:
593 width
= parent
.width()
594 height
= parent
.height()
595 return (width
, height
)
598 def default_monospace_font():
601 if utils
.is_darwin():
603 font
.setFamily(family
)
608 font_str
= gitcfg
.current().get(prefs
.FONTDIFF
)
610 font_str
= default_monospace_font().toString()
615 return font(diff_font_str())
620 font
.fromString(string
)
624 def create_button(text
='', layout
=None, tooltip
=None, icon
=None,
625 enabled
=True, default
=False):
626 """Create a button, set its title, and add it to the parent."""
627 button
= QtWidgets
.QPushButton()
628 button
.setCursor(Qt
.PointingHandCursor
)
633 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
634 if tooltip
is not None:
635 button
.setToolTip(tooltip
)
636 if layout
is not None:
637 layout
.addWidget(button
)
639 button
.setEnabled(False)
641 button
.setDefault(True)
645 def create_action_button(tooltip
=None, icon
=None):
646 button
= QtWidgets
.QPushButton()
647 button
.setCursor(Qt
.PointingHandCursor
)
649 if tooltip
is not None:
650 button
.setToolTip(tooltip
)
653 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
657 def ok_button(text
, default
=True, enabled
=True, icon
=None):
660 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
664 return create_button(text
=N_('Close'), icon
=icons
.close())
667 def edit_button(enabled
=True, default
=False):
668 return create_button(text
=N_('Edit'), icon
=icons
.edit(),
669 enabled
=enabled
, default
=default
)
672 def refresh_button(enabled
=True, default
=False):
673 return create_button(text
=N_('Refresh'), icon
=icons
.sync(),
674 enabled
=enabled
, default
=default
)
677 def hide_button_menu_indicator(button
):
681 %(name)s::menu-indicator {
685 if name
== 'QPushButton':
691 button
.setStyleSheet(stylesheet
% {'name': name
})
694 def checkbox(text
='', tooltip
='', checked
=None):
695 cb
= QtWidgets
.QCheckBox()
699 cb
.setToolTip(tooltip
)
700 if checked
is not None:
701 cb
.setChecked(checked
)
703 url
= icons
.check_name()
705 QCheckBox::indicator {
709 QCheckBox::indicator::unchecked {
710 border: %(border)dpx solid #999;
713 QCheckBox::indicator::checked {
715 border: %(border)dpx solid black;
718 """ % dict(size
=defs
.checkbox
, border
=defs
.border
, url
=url
)
719 cb
.setStyleSheet(style
)
724 def radio(text
='', tooltip
='', checked
=None):
725 rb
= QtWidgets
.QRadioButton()
729 rb
.setToolTip(tooltip
)
730 if checked
is not None:
731 rb
.setChecked(checked
)
735 border
= defs
.radio_border
736 url
= icons
.dot_name()
738 QRadioButton::indicator {
742 QRadioButton::indicator::unchecked {
744 border: %(border)dpx solid #999;
745 border-radius: %(radius)dpx;
747 QRadioButton::indicator::checked {
750 border: %(border)dpx solid black;
751 border-radius: %(radius)dpx;
753 """ % dict(size
=size
, radius
=radius
, border
=border
, url
=url
)
754 rb
.setStyleSheet(style
)
759 class DockTitleBarWidget(QtWidgets
.QWidget
):
761 def __init__(self
, parent
, title
, stretch
=True):
762 QtWidgets
.QWidget
.__init
__(self
, parent
)
763 self
.label
= qlabel
= QtWidgets
.QLabel()
767 qlabel
.setText(title
)
768 qlabel
.setCursor(Qt
.OpenHandCursor
)
770 self
.close_button
= create_action_button(
771 tooltip
=N_('Close'), icon
=icons
.close())
773 self
.toggle_button
= create_action_button(
774 tooltip
=N_('Detach'), icon
=icons
.external())
776 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
783 self
.main_layout
= hbox(defs
.small_margin
, defs
.spacing
,
784 qlabel
, separator
, self
.corner_layout
,
785 self
.toggle_button
, self
.close_button
)
786 self
.setLayout(self
.main_layout
)
788 connect_button(self
.toggle_button
, self
.toggle_floating
)
789 connect_button(self
.close_button
, self
.toggle_visibility
)
791 def toggle_floating(self
):
792 self
.parent().setFloating(not self
.parent().isFloating())
793 self
.update_tooltips()
795 def toggle_visibility(self
):
796 self
.parent().toggleViewAction().trigger()
798 def set_title(self
, title
):
799 self
.label
.setText(title
)
801 def add_corner_widget(self
, widget
):
802 self
.corner_layout
.addWidget(widget
)
804 def update_tooltips(self
):
805 if self
.parent().isFloating():
806 tooltip
= N_('Attach')
808 tooltip
= N_('Detach')
809 self
.toggle_button
.setToolTip(tooltip
)
812 def create_dock(title
, parent
, stretch
=True):
813 """Create a dock widget and set it up accordingly."""
814 dock
= QtWidgets
.QDockWidget(parent
)
815 dock
.setWindowTitle(title
)
816 dock
.setObjectName(title
)
817 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
818 dock
.setTitleBarWidget(titlebar
)
819 if hasattr(parent
, 'dockwidgets'):
820 parent
.dockwidgets
.append(dock
)
824 def create_menu(title
, parent
):
825 """Create a menu and set its title."""
826 qmenu
= QtWidgets
.QMenu(title
, parent
)
830 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
831 button
= QtWidgets
.QToolButton()
832 button
.setAutoRaise(True)
833 button
.setAutoFillBackground(True)
834 button
.setCursor(Qt
.PointingHandCursor
)
837 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
840 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
841 if tooltip
is not None:
842 button
.setToolTip(tooltip
)
843 if layout
is not None:
844 layout
.addWidget(button
)
848 def mimedata_from_paths(paths
):
849 """Return mimedata with a list of absolute path URLs"""
851 abspaths
= [core
.abspath(path
) for path
in paths
]
852 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
854 mimedata
= QtCore
.QMimeData()
855 mimedata
.setUrls(urls
)
857 # The text/x-moz-list format is always included by Qt, and doing
858 # mimedata.removeFormat('text/x-moz-url') has no effect.
859 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
861 # gnome-terminal expects utf-16 encoded text, but other terminals,
862 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
863 # to override the default.
864 paths_text
= core
.list2cmdline(abspaths
)
865 encoding
= gitcfg
.current().get('cola.dragencoding', 'utf-16')
866 moz_text
= core
.encode(paths_text
, encoding
=encoding
)
867 mimedata
.setData('text/x-moz-url', moz_text
)
872 def path_mimetypes():
873 return ['text/uri-list', 'text/x-moz-url']
876 class BlockSignals(object):
877 """Context manager for blocking a signals on a widget"""
879 def __init__(self
, *widgets
):
880 self
.widgets
= widgets
884 for w
in self
.widgets
:
885 self
.values
[w
] = w
.blockSignals(True)
888 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
889 for w
in self
.widgets
:
890 w
.blockSignals(self
.values
[w
])
893 class Channel(QtCore
.QObject
):
894 finished
= Signal(object)
895 result
= Signal(object)
898 class Task(QtCore
.QRunnable
):
899 """Disable auto-deletion to avoid gc issues
901 Python's garbage collector will try to double-free the task
902 once it's finished, so disable Qt's auto-deletion as a workaround.
906 def __init__(self
, parent
):
907 QtCore
.QRunnable
.__init
__(self
)
909 self
.channel
= Channel()
911 self
.setAutoDelete(False)
914 self
.result
= self
.task()
915 self
.channel
.result
.emit(self
.result
)
922 self
.channel
.finished
.emit(self
)
924 def connect(self
, handler
):
925 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
928 class SimpleTask(Task
):
929 """Run a simple callable as a task"""
931 def __init__(self
, parent
, fn
, *args
, **kwargs
):
932 Task
.__init
__(self
, parent
)
939 return self
.fn(*self
.args
, **self
.kwargs
)
942 class RunTask(QtCore
.QObject
):
943 """Runs QRunnable instances and transfers control when they finish"""
945 def __init__(self
, parent
=None):
946 QtCore
.QObject
.__init
__(self
, parent
)
948 self
.task_details
= {}
949 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
951 def start(self
, task
, progress
=None, finish
=None):
952 """Start the task and register a callback"""
953 if progress
is not None:
955 # prevents garbage collection bugs in certain PyQt4 versions
956 self
.tasks
.append(task
)
958 self
.task_details
[task_id
] = (progress
, finish
)
960 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
961 self
.threadpool
.start(task
)
963 def finish(self
, task
):
966 self
.tasks
.remove(task
)
970 progress
, finish
= self
.task_details
[task_id
]
971 del self
.task_details
[task_id
]
973 finish
= progress
= None
975 if progress
is not None:
978 if finish
is not None:
982 # Syntax highlighting
984 def rgba(r
, g
, b
, a
=255):
995 def make_format(fg
=None, bg
=None, bold
=False):
996 fmt
= QtGui
.QTextCharFormat()
998 fmt
.setForeground(fg
)
1000 fmt
.setBackground(bg
)
1002 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1007 Interaction
.critical
= staticmethod(critical
)
1008 Interaction
.confirm
= staticmethod(confirm
)
1009 Interaction
.question
= staticmethod(question
)
1010 Interaction
.information
= staticmethod(information
)