1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
4 from __future__
import division
, absolute_import
, unicode_literals
8 from PyQt4
import QtGui
9 from PyQt4
import QtCore
10 from PyQt4
.QtCore
import Qt
11 from PyQt4
.QtCore
import SIGNAL
14 from cola
import gitcfg
15 from cola
import hotkeys
16 from cola
import icons
17 from cola
import utils
18 from cola
import resources
19 from cola
.i18n
import N_
20 from cola
.interaction
import Interaction
21 from cola
.compat
import ustr
22 from cola
.compat
import PY3
23 from cola
.models
import prefs
24 from cola
.widgets
import defs
27 def connect_action(action
, fn
):
28 """Connectc an action to a function"""
29 action
.connect(action
, SIGNAL('triggered()'), fn
)
32 def connect_action_bool(action
, fn
):
33 """Connect a triggered(bool) action to a function"""
34 action
.connect(action
, SIGNAL('triggered(bool)'), fn
)
37 def connect_button(button
, fn
):
38 """Connect a button to a function"""
39 button
.connect(button
, SIGNAL('clicked()'), fn
)
42 def button_action(button
, action
):
43 """Make a button trigger an action"""
44 connect_button(button
, action
.trigger
)
47 def connect_toggle(toggle
, fn
):
48 toggle
.connect(toggle
, SIGNAL('toggled(bool)'), fn
)
52 return QtGui
.QApplication
.activeWindow()
55 def hbox(margin
, spacing
, *items
):
56 return box(QtGui
.QHBoxLayout
, margin
, spacing
, *items
)
59 def vbox(margin
, spacing
, *items
):
60 return box(QtGui
.QVBoxLayout
, margin
, spacing
, *items
)
63 def buttongroup(*items
):
64 group
= QtGui
.QButtonGroup()
74 def box(cls
, margin
, spacing
, *items
):
78 layout
.setMargin(margin
)
79 layout
.setSpacing(spacing
)
84 int_types
= (int, long)
87 if isinstance(i
, QtGui
.QWidget
):
89 elif isinstance(i
, (QtGui
.QHBoxLayout
, QtGui
.QVBoxLayout
,
90 QtGui
.QFormLayout
, QtGui
.QLayout
)):
96 elif isinstance(i
, int_types
):
102 def form(margin
, spacing
, *widgets
):
103 layout
= QtGui
.QFormLayout()
104 layout
.setMargin(margin
)
105 layout
.setSpacing(spacing
)
106 layout
.setFieldGrowthPolicy(QtGui
.QFormLayout
.ExpandingFieldsGrow
)
108 for idx
, (label
, widget
) in enumerate(widgets
):
109 if isinstance(label
, (str, ustr
)):
110 layout
.addRow(label
, widget
)
112 layout
.setWidget(idx
, QtGui
.QFormLayout
.LabelRole
, label
)
113 layout
.setWidget(idx
, QtGui
.QFormLayout
.FieldRole
, widget
)
118 def grid(margin
, spacing
, *widgets
):
119 layout
= QtGui
.QGridLayout()
120 layout
.setMargin(defs
.no_margin
)
121 layout
.setSpacing(defs
.spacing
)
125 if isinstance(item
, QtGui
.QWidget
):
126 layout
.addWidget(*row
)
127 elif isinstance(item
, QtGui
.QLayoutItem
):
133 def splitter(orientation
, *widgets
):
134 layout
= QtGui
.QSplitter()
135 layout
.setOrientation(orientation
)
136 layout
.setHandleWidth(defs
.handle_width
)
137 layout
.setChildrenCollapsible(True)
138 for idx
, widget
in enumerate(widgets
):
139 layout
.addWidget(widget
)
140 layout
.setStretchFactor(idx
, 1)
145 def prompt(msg
, title
=None, text
=''):
146 """Presents the user with an input widget and returns the input."""
149 result
= QtGui
.QInputDialog
.getText(active_window(), msg
, title
,
150 QtGui
.QLineEdit
.Normal
, text
)
151 return (ustr(result
[0]), result
[1])
154 def create_listwidget_item(text
, filename
):
155 """Creates a QListWidgetItem with text and the icon at filename."""
156 item
= QtGui
.QListWidgetItem()
157 item
.setIcon(QtGui
.QIcon(filename
))
158 item
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
163 class TreeWidgetItem(QtGui
.QTreeWidgetItem
):
165 TYPE
= QtGui
.QStandardItem
.UserType
+ 101
167 def __init__(self
, path
, icon
, deleted
):
168 QtGui
.QTreeWidgetItem
.__init
__(self
)
170 self
.deleted
= deleted
171 self
.setIcon(0, icons
.from_name(icon
))
172 self
.setText(0, path
)
178 def paths_from_indexes(model
, indexes
,
179 item_type
=TreeWidgetItem
.TYPE
,
181 """Return paths from a list of QStandardItemModel indexes"""
182 items
= [model
.itemFromIndex(i
) for i
in indexes
]
183 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
186 def paths_from_items(items
,
187 item_type
=TreeWidgetItem
.TYPE
,
189 """Return a list of paths from a list of items"""
190 if item_filter
is None:
191 item_filter
= lambda x
: True
192 return [i
.path
for i
in items
193 if i
.type() == item_type
and item_filter(i
)]
196 def confirm(title
, text
, informative_text
, ok_text
,
197 icon
=None, default
=True,
198 cancel_text
=None, cancel_icon
=None):
199 """Confirm that an action should take place"""
200 msgbox
= QtGui
.QMessageBox(active_window())
201 msgbox
.setWindowModality(Qt
.WindowModal
)
202 msgbox
.setWindowTitle(title
)
204 msgbox
.setInformativeText(informative_text
)
206 icon
= icons
.mkicon(icon
, icons
.ok
)
207 ok
= msgbox
.addButton(ok_text
, QtGui
.QMessageBox
.ActionRole
)
210 cancel
= msgbox
.addButton(QtGui
.QMessageBox
.Cancel
)
211 cancel_icon
= icons
.mkicon(cancel_icon
, icons
.close
)
212 cancel
.setIcon(cancel_icon
)
214 cancel
.setText(cancel_text
)
217 msgbox
.setDefaultButton(ok
)
219 msgbox
.setDefaultButton(cancel
)
221 return msgbox
.clickedButton() == ok
224 class ResizeableMessageBox(QtGui
.QMessageBox
):
226 def __init__(self
, parent
):
227 QtGui
.QMessageBox
.__init
__(self
, parent
)
228 self
.setMouseTracking(True)
229 self
.setSizeGripEnabled(True)
231 def event(self
, event
):
232 res
= QtGui
.QMessageBox
.event(self
, event
)
233 event_type
= event
.type()
234 if (event_type
== QtCore
.QEvent
.MouseMove
or
235 event_type
== QtCore
.QEvent
.MouseButtonPress
):
236 maxi
= QtCore
.QSize(defs
.max_size
, defs
.max_size
)
237 self
.setMaximumSize(maxi
)
238 text
= self
.findChild(QtGui
.QTextEdit
)
240 expand
= QtGui
.QSizePolicy
.Expanding
241 text
.setSizePolicy(QtGui
.QSizePolicy(expand
, expand
))
242 text
.setMaximumSize(maxi
)
246 def critical(title
, message
=None, details
=None):
247 """Show a warning with the provided title and message."""
250 mbox
= ResizeableMessageBox(active_window())
251 mbox
.setWindowTitle(title
)
252 mbox
.setTextFormat(Qt
.PlainText
)
253 mbox
.setText(message
)
254 mbox
.setIcon(QtGui
.QMessageBox
.Critical
)
255 mbox
.setStandardButtons(QtGui
.QMessageBox
.Close
)
256 mbox
.setDefaultButton(QtGui
.QMessageBox
.Close
)
258 mbox
.setDetailedText(details
)
262 def information(title
, message
=None, details
=None, informative_text
=None):
263 """Show information with the provided title and message."""
266 mbox
= QtGui
.QMessageBox(active_window())
267 mbox
.setStandardButtons(QtGui
.QMessageBox
.Close
)
268 mbox
.setDefaultButton(QtGui
.QMessageBox
.Close
)
269 mbox
.setWindowTitle(title
)
270 mbox
.setWindowModality(Qt
.WindowModal
)
271 mbox
.setTextFormat(Qt
.PlainText
)
272 mbox
.setText(message
)
274 mbox
.setInformativeText(informative_text
)
276 mbox
.setDetailedText(details
)
277 # Render into a 1-inch wide pixmap
278 pixmap
= icons
.cola().pixmap(defs
.large_icon
)
279 mbox
.setIconPixmap(pixmap
)
283 def question(title
, msg
, default
=True):
284 """Launches a QMessageBox question with the provided title and message.
285 Passing "default=False" will make "No" the default choice."""
286 yes
= QtGui
.QMessageBox
.Yes
287 no
= QtGui
.QMessageBox
.No
293 result
= (QtGui
.QMessageBox
294 .question(active_window(), title
, msg
, buttons
, default
))
295 return result
== QtGui
.QMessageBox
.Yes
298 def tree_selection(tree_item
, items
):
299 """Returns an array of model items that correspond to the selected
300 QTreeWidgetItem children"""
302 count
= min(tree_item
.childCount(), len(items
))
303 for idx
in range(count
):
304 if tree_item
.child(idx
).isSelected():
305 selected
.append(items
[idx
])
310 def tree_selection_items(tree_item
):
311 """Returns selected widget items"""
313 for idx
in range(tree_item
.childCount()):
314 child
= tree_item
.child(idx
)
315 if child
.isSelected():
316 selected
.append(child
)
321 def selected_item(list_widget
, items
):
322 """Returns the model item that corresponds to the selected QListWidget
324 widget_items
= list_widget
.selectedItems()
327 widget_item
= widget_items
[0]
328 row
= list_widget
.row(widget_item
)
335 def selected_items(list_widget
, items
):
336 """Returns an array of model items that correspond to the selected
338 item_count
= len(items
)
340 for widget_item
in list_widget
.selectedItems():
341 row
= list_widget
.row(widget_item
)
343 selected
.append(items
[row
])
347 def open_file(title
, directory
=None):
348 """Creates an Open File dialog and returns a filename."""
349 return ustr(QtGui
.QFileDialog
350 .getOpenFileName(active_window(), title
, directory
))
353 def open_files(title
, directory
=None, filter=None):
354 """Creates an Open File dialog and returns a list of filenames."""
355 return (QtGui
.QFileDialog
356 .getOpenFileNames(active_window(), title
, directory
, filter))
359 def opendir_dialog(title
, path
):
360 """Prompts for a directory path"""
362 flags
= (QtGui
.QFileDialog
.ShowDirsOnly |
363 QtGui
.QFileDialog
.DontResolveSymlinks
)
364 return ustr(QtGui
.QFileDialog
365 .getExistingDirectory(active_window(),
369 def save_as(filename
, title
='Save As...'):
370 """Creates a Save File dialog and returns a filename."""
371 return ustr(QtGui
.QFileDialog
372 .getSaveFileName(active_window(), title
, filename
))
375 def copy_path(filename
, absolute
=True):
376 """Copy a filename to the clipboard"""
380 filename
= core
.abspath(filename
)
381 set_clipboard(filename
)
384 def set_clipboard(text
):
385 """Sets the copy/paste buffer to text."""
388 clipboard
= QtGui
.QApplication
.clipboard()
389 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
390 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
394 def persist_clipboard():
395 """Persist the clipboard
397 X11 stores only a reference to the clipboard data.
398 Send a clipboard event to force a copy of the clipboard to occur.
399 This ensures that the clipboard is present after git-cola exits.
400 Otherwise, the reference is destroyed on exit.
402 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
405 clipboard
= QtGui
.QApplication
.clipboard()
406 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
407 QtGui
.QApplication
.sendEvent(clipboard
, event
)
410 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
412 action
= _add_action(widget
, text
, tip
, fn
, connect_action_bool
, *shortcuts
)
413 action
.setCheckable(True)
414 action
.setChecked(checked
)
418 def add_action(widget
, text
, fn
, *shortcuts
):
420 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
423 def add_action_with_status_tip(widget
, text
, tip
, fn
, *shortcuts
):
424 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
427 def _add_action(widget
, text
, tip
, fn
, connect
, *shortcuts
):
428 action
= QtGui
.QAction(text
, widget
)
430 action
.setStatusTip(tip
)
433 action
.setShortcuts(shortcuts
)
434 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
435 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
436 widget
.addAction(action
)
440 def set_selected_item(widget
, idx
):
441 """Sets a the currently selected item to the item at index idx."""
442 if type(widget
) is QtGui
.QTreeWidget
:
443 item
= widget
.topLevelItem(idx
)
445 widget
.setItemSelected(item
, True)
446 widget
.setCurrentItem(item
)
449 def add_items(widget
, items
):
450 """Adds items to a widget."""
457 def set_items(widget
, items
):
458 """Clear the existing widget contents and set the new items."""
460 add_items(widget
, items
)
464 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
465 """Given a filename, return a TreeWidgetItem for a status widget
467 "staged", "deleted, and "untracked" control which icon is used.
470 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
471 return TreeWidgetItem(filename
, resources
.icon(icon_name
), deleted
=deleted
)
474 def add_close_action(widget
):
475 """Adds close action and shortcuts to a widget."""
476 return add_action(widget
, N_('Close...'),
477 widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
480 def center_on_screen(widget
):
481 """Move widget to the center of the default screen"""
482 desktop
= QtGui
.QApplication
.instance().desktop()
483 rect
= desktop
.screenGeometry(QtGui
.QCursor().pos())
484 cy
= rect
.height()//2
486 widget
.move(cx
- widget
.width()//2, cy
- widget
.height()//2)
489 def default_size(parent
, width
, height
):
490 """Return the parent's size, or the provided defaults"""
491 if parent
is not None:
492 width
= parent
.width()
493 height
= parent
.height()
494 return (width
, height
)
497 def default_monospace_font():
500 if utils
.is_darwin():
502 font
.setFamily(family
)
507 font_str
= gitcfg
.current().get(prefs
.FONTDIFF
)
509 font
= default_monospace_font()
510 font_str
= ustr(font
.toString())
515 return font(diff_font_str())
520 font
.fromString(string
)
524 def create_button(text
='', layout
=None, tooltip
=None, icon
=None,
525 enabled
=True, default
=False):
526 """Create a button, set its title, and add it to the parent."""
527 button
= QtGui
.QPushButton()
528 button
.setCursor(Qt
.PointingHandCursor
)
533 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
534 if tooltip
is not None:
535 button
.setToolTip(tooltip
)
536 if layout
is not None:
537 layout
.addWidget(button
)
539 button
.setEnabled(False)
541 button
.setDefault(True)
545 def create_action_button(tooltip
=None, icon
=None):
546 button
= QtGui
.QPushButton()
547 button
.setCursor(Qt
.PointingHandCursor
)
549 if tooltip
is not None:
550 button
.setToolTip(tooltip
)
553 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
557 def ok_button(text
, default
=False, enabled
=True):
558 return create_button(text
=text
, icon
=icons
.ok(),
559 default
=default
, enabled
=enabled
)
563 return create_button(text
=N_('Close'), icon
=icons
.close())
566 def edit_button(enabled
=True, default
=False):
567 return create_button(text
=N_('Edit'), icon
=icons
.edit(),
568 enabled
=enabled
, default
=default
)
571 def refresh_button(enabled
=True, default
=False):
572 return create_button(text
=N_('Refresh'), icon
=icons
.sync(),
573 enabled
=enabled
, default
=default
)
576 def hide_button_menu_indicator(button
):
580 %(name)s::menu-indicator {
584 if name
== 'QPushButton':
590 button
.setStyleSheet(stylesheet
% {'name': name
})
593 def checkbox(text
='', tooltip
='', checked
=None):
594 cb
= QtGui
.QCheckBox()
598 cb
.setToolTip(tooltip
)
599 if checked
is not None:
600 cb
.setChecked(checked
)
602 url
= icons
.check_name()
604 QCheckBox::indicator {
608 QCheckBox::indicator::unchecked {
609 border: %(border)dpx solid #999;
612 QCheckBox::indicator::checked {
614 border: %(border)dpx solid black;
617 """ % dict(size
=defs
.checkbox
, border
=defs
.border
, url
=url
)
618 cb
.setStyleSheet(style
)
623 def radio(text
='', tooltip
='', checked
=None):
624 rb
= QtGui
.QRadioButton()
628 rb
.setToolTip(tooltip
)
629 if checked
is not None:
630 rb
.setChecked(checked
)
634 border
= defs
.radio_border
635 url
= icons
.dot_name()
637 QRadioButton::indicator {
641 QRadioButton::indicator::unchecked {
643 border: %(border)dpx solid #999;
644 border-radius: %(radius)dpx;
646 QRadioButton::indicator::checked {
649 border: %(border)dpx solid black;
650 border-radius: %(radius)dpx;
652 """ % dict(size
=size
, radius
=radius
, border
=border
, url
=url
)
653 rb
.setStyleSheet(style
)
658 class DockTitleBarWidget(QtGui
.QWidget
):
660 def __init__(self
, parent
, title
, stretch
=True):
661 QtGui
.QWidget
.__init
__(self
, parent
)
662 self
.label
= label
= QtGui
.QLabel()
667 label
.setCursor(Qt
.OpenHandCursor
)
669 self
.close_button
= create_action_button(
670 tooltip
=N_('Close'), icon
=icons
.close())
672 self
.toggle_button
= create_action_button(
673 tooltip
=N_('Detach'), icon
=icons
.external())
675 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
682 self
.main_layout
= hbox(defs
.small_margin
, defs
.spacing
,
683 label
, separator
, self
.corner_layout
,
684 self
.toggle_button
, self
.close_button
)
685 self
.setLayout(self
.main_layout
)
687 connect_button(self
.toggle_button
, self
.toggle_floating
)
688 connect_button(self
.close_button
, self
.toggle_visibility
)
690 def toggle_floating(self
):
691 self
.parent().setFloating(not self
.parent().isFloating())
692 self
.update_tooltips()
694 def toggle_visibility(self
):
695 self
.parent().toggleViewAction().trigger()
697 def set_title(self
, title
):
698 self
.label
.setText(title
)
700 def add_corner_widget(self
, widget
):
701 self
.corner_layout
.addWidget(widget
)
703 def update_tooltips(self
):
704 if self
.parent().isFloating():
705 tooltip
= N_('Attach')
707 tooltip
= N_('Detach')
708 self
.toggle_button
.setToolTip(tooltip
)
711 def create_dock(title
, parent
, stretch
=True):
712 """Create a dock widget and set it up accordingly."""
713 dock
= QtGui
.QDockWidget(parent
)
714 dock
.setWindowTitle(title
)
715 dock
.setObjectName(title
)
716 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
717 dock
.setTitleBarWidget(titlebar
)
718 if hasattr(parent
, 'dockwidgets'):
719 parent
.dockwidgets
.append(dock
)
723 def create_menu(title
, parent
):
724 """Create a menu and set its title."""
725 qmenu
= QtGui
.QMenu(parent
)
726 qmenu
.setTitle(title
)
730 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
731 button
= QtGui
.QToolButton()
732 button
.setAutoRaise(True)
733 button
.setAutoFillBackground(True)
734 button
.setCursor(Qt
.PointingHandCursor
)
737 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
740 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
741 if tooltip
is not None:
742 button
.setToolTip(tooltip
)
743 if layout
is not None:
744 layout
.addWidget(button
)
748 def mimedata_from_paths(paths
):
749 """Return mimedata with a list of absolute path URLs"""
751 abspaths
= [core
.abspath(path
) for path
in paths
]
752 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
754 mimedata
= QtCore
.QMimeData()
755 mimedata
.setUrls(urls
)
757 # The text/x-moz-list format is always included by Qt, and doing
758 # mimedata.removeFormat('text/x-moz-url') has no effect.
759 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
761 # gnome-terminal expects utf-16 encoded text, but other terminals,
762 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
763 # to override the default.
764 paths_text
= subprocess
.list2cmdline(abspaths
)
765 encoding
= gitcfg
.current().get('cola.dragencoding', 'utf-16')
766 moz_text
= core
.encode(paths_text
, encoding
=encoding
)
767 mimedata
.setData('text/x-moz-url', moz_text
)
772 def path_mimetypes():
773 return ['text/uri-list', 'text/x-moz-url']
776 class BlockSignals(object):
777 """Context manager for blocking a signals on a widget"""
779 def __init__(self
, *widgets
):
780 self
.widgets
= widgets
784 for w
in self
.widgets
:
785 self
.values
[w
] = w
.blockSignals(True)
788 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
789 for w
in self
.widgets
:
790 w
.blockSignals(self
.values
[w
])
793 class Task(QtCore
.QRunnable
):
794 """Disable auto-deletion to avoid gc issues
796 Python's garbage collector will try to double-free the task
797 once it's finished, so disable Qt's auto-deletion as a workaround.
801 FINISHED
= SIGNAL('TASK_FINISHED')
802 RESULT
= SIGNAL('TASK_RESULT')
804 def __init__(self
, parent
, *args
, **kwargs
):
805 QtCore
.QRunnable
.__init
__(self
)
807 self
.channel
= QtCore
.QObject(parent
)
809 self
.setAutoDelete(False)
812 self
.result
= self
.task()
813 self
.channel
.emit(self
.RESULT
, self
.result
)
820 self
.channel
.emit(self
.FINISHED
, self
)
822 def connect(self
, handler
):
823 self
.channel
.connect(self
.channel
, self
.RESULT
,
824 handler
, Qt
.QueuedConnection
)
827 class SimpleTask(Task
):
828 """Run a simple callable as a task"""
830 def __init__(self
, parent
, fn
, *args
, **kwargs
):
831 Task
.__init
__(self
, parent
)
838 return self
.fn(*self
.args
, **self
.kwargs
)
841 class RunTask(QtCore
.QObject
):
842 """Runs QRunnable instances and transfers control when they finish"""
844 def __init__(self
, parent
=None):
845 QtCore
.QObject
.__init
__(self
, parent
)
847 self
.task_details
= {}
848 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
850 def start(self
, task
, progress
=None, finish
=None):
851 """Start the task and register a callback"""
852 if progress
is not None:
854 # prevents garbage collection bugs in certain PyQt4 versions
855 self
.tasks
.append(task
)
857 self
.task_details
[task_id
] = (progress
, finish
)
859 self
.connect(task
.channel
, Task
.FINISHED
, self
.finish
,
861 self
.threadpool
.start(task
)
863 def finish(self
, task
, *args
, **kwargs
):
866 self
.tasks
.remove(task
)
870 progress
, finish
= self
.task_details
[task_id
]
871 del self
.task_details
[task_id
]
873 finish
= progress
= None
875 if progress
is not None:
878 if finish
is not None:
879 finish(task
, *args
, **kwargs
)
882 # Syntax highlighting
884 def rgba(r
, g
, b
, a
=255):
895 def make_format(fg
=None, bg
=None, bold
=False):
896 fmt
= QtGui
.QTextCharFormat()
898 fmt
.setForeground(fg
)
900 fmt
.setBackground(bg
)
902 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
907 Interaction
.critical
= staticmethod(critical
)
908 Interaction
.confirm
= staticmethod(confirm
)
909 Interaction
.question
= staticmethod(question
)
910 Interaction
.information
= staticmethod(information
)