status: use Italics instead of Bold-with-background for headers
[git-cola.git] / cola / qtutils.py
blobb1663618ee36c171a182059028296b913b19b33e
1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
3 """
4 from __future__ import division, absolute_import, unicode_literals
6 import subprocess
8 from PyQt4 import QtGui
9 from PyQt4 import QtCore
10 from PyQt4.QtCore import Qt
11 from PyQt4.QtCore import SIGNAL
13 from cola import core
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.models import prefs
23 from cola.widgets import defs
26 def connect_action(action, fn):
27 """Connectc an action to a function"""
28 action.connect(action, SIGNAL('triggered()'), fn)
31 def connect_action_bool(action, fn):
32 """Connect a triggered(bool) action to a function"""
33 action.connect(action, SIGNAL('triggered(bool)'), fn)
36 def connect_button(button, fn):
37 """Connect a button to a function"""
38 button.connect(button, SIGNAL('clicked()'), fn)
41 def button_action(button, action):
42 """Make a button trigger an action"""
43 connect_button(button, action.trigger)
46 def connect_toggle(toggle, fn):
47 toggle.connect(toggle, SIGNAL('toggled(bool)'), fn)
50 def active_window():
51 return QtGui.QApplication.activeWindow()
54 def hbox(margin, spacing, *items):
55 return box(QtGui.QHBoxLayout, margin, spacing, *items)
58 def vbox(margin, spacing, *items):
59 return box(QtGui.QVBoxLayout, margin, spacing, *items)
62 STRETCH = object()
63 SKIPPED = object()
66 def box(cls, margin, spacing, *items):
67 stretch = STRETCH
68 skipped = SKIPPED
69 layout = cls()
70 layout.setMargin(margin)
71 layout.setSpacing(spacing)
73 for i in items:
74 if isinstance(i, QtGui.QWidget):
75 layout.addWidget(i)
76 elif isinstance(i, (QtGui.QHBoxLayout, QtGui.QVBoxLayout,
77 QtGui.QFormLayout, QtGui.QLayout)):
78 layout.addLayout(i)
79 elif i is stretch:
80 layout.addStretch()
81 elif i is skipped:
82 continue
83 elif isinstance(i, (int, long)):
84 layout.addSpacing(i)
86 return layout
89 def form(margin, spacing, *widgets):
90 layout = QtGui.QFormLayout()
91 layout.setMargin(margin)
92 layout.setSpacing(spacing)
93 layout.setFieldGrowthPolicy(QtGui.QFormLayout.ExpandingFieldsGrow)
95 for idx, (label, widget) in enumerate(widgets):
96 if isinstance(label, (str, ustr)):
97 layout.addRow(label, widget)
98 else:
99 layout.setWidget(idx, QtGui.QFormLayout.LabelRole, label)
100 layout.setWidget(idx, QtGui.QFormLayout.FieldRole, widget)
102 return layout
105 def grid(margin, spacing, *widgets):
106 layout = QtGui.QGridLayout()
107 layout.setMargin(defs.no_margin)
108 layout.setSpacing(defs.spacing)
110 for row in widgets:
111 item = row[0]
112 if isinstance(item, QtGui.QWidget):
113 layout.addWidget(*row)
114 elif isinstance(item, QtGui.QLayoutItem):
115 layout.addItem(*row)
117 return layout
120 def splitter(orientation, *widgets):
121 layout = QtGui.QSplitter()
122 layout.setOrientation(orientation)
123 layout.setHandleWidth(defs.handle_width)
124 layout.setChildrenCollapsible(True)
125 for idx, widget in enumerate(widgets):
126 layout.addWidget(widget)
127 layout.setStretchFactor(idx, 1)
129 return layout
132 def prompt(msg, title=None, text=''):
133 """Presents the user with an input widget and returns the input."""
134 if title is None:
135 title = msg
136 result = QtGui.QInputDialog.getText(active_window(), msg, title,
137 QtGui.QLineEdit.Normal, text)
138 return (ustr(result[0]), result[1])
141 def create_listwidget_item(text, filename):
142 """Creates a QListWidgetItem with text and the icon at filename."""
143 item = QtGui.QListWidgetItem()
144 item.setIcon(QtGui.QIcon(filename))
145 item.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
146 item.setText(text)
147 return item
150 class TreeWidgetItem(QtGui.QTreeWidgetItem):
152 TYPE = QtGui.QStandardItem.UserType + 101
154 def __init__(self, path, icon, deleted):
155 QtGui.QTreeWidgetItem.__init__(self)
156 self.path = path
157 self.deleted = deleted
158 self.setIcon(0, icons.from_name(icon))
159 self.setText(0, path)
161 def type(self):
162 return self.TYPE
165 def paths_from_indexes(model, indexes,
166 item_type=TreeWidgetItem.TYPE,
167 item_filter=None):
168 """Return paths from a list of QStandardItemModel indexes"""
169 items = [model.itemFromIndex(i) for i in indexes]
170 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
173 def paths_from_items(items,
174 item_type=TreeWidgetItem.TYPE,
175 item_filter=None):
176 """Return a list of paths from a list of items"""
177 if item_filter is None:
178 item_filter = lambda x: True
179 return [i.path for i in items
180 if i.type() == item_type and item_filter(i)]
183 def confirm(title, text, informative_text, ok_text,
184 icon=None, default=True,
185 cancel_text=None, cancel_icon=None):
186 """Confirm that an action should take place"""
187 msgbox = QtGui.QMessageBox(active_window())
188 msgbox.setWindowModality(Qt.WindowModal)
189 msgbox.setWindowTitle(title)
190 msgbox.setText(text)
191 msgbox.setInformativeText(informative_text)
193 icon = icons.mkicon(icon, icons.ok)
194 ok = msgbox.addButton(ok_text, QtGui.QMessageBox.ActionRole)
195 ok.setIcon(icon)
197 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
198 cancel_icon = icons.mkicon(cancel_icon, icons.close)
199 cancel.setIcon(cancel_icon)
200 if cancel_text:
201 cancel.setText(cancel_text)
203 if default:
204 msgbox.setDefaultButton(ok)
205 else:
206 msgbox.setDefaultButton(cancel)
207 msgbox.exec_()
208 return msgbox.clickedButton() == ok
211 class ResizeableMessageBox(QtGui.QMessageBox):
213 def __init__(self, parent):
214 QtGui.QMessageBox.__init__(self, parent)
215 self.setMouseTracking(True)
216 self.setSizeGripEnabled(True)
218 def event(self, event):
219 res = QtGui.QMessageBox.event(self, event)
220 event_type = event.type()
221 if (event_type == QtCore.QEvent.MouseMove or
222 event_type == QtCore.QEvent.MouseButtonPress):
223 maxi = QtCore.QSize(defs.max_size, defs.max_size)
224 self.setMaximumSize(maxi)
225 text = self.findChild(QtGui.QTextEdit)
226 if text is not None:
227 expand = QtGui.QSizePolicy.Expanding
228 text.setSizePolicy(QtGui.QSizePolicy(expand, expand))
229 text.setMaximumSize(maxi)
230 return res
233 def critical(title, message=None, details=None):
234 """Show a warning with the provided title and message."""
235 if message is None:
236 message = title
237 mbox = ResizeableMessageBox(active_window())
238 mbox.setWindowTitle(title)
239 mbox.setTextFormat(Qt.PlainText)
240 mbox.setText(message)
241 mbox.setIcon(QtGui.QMessageBox.Critical)
242 mbox.setStandardButtons(QtGui.QMessageBox.Close)
243 mbox.setDefaultButton(QtGui.QMessageBox.Close)
244 if details:
245 mbox.setDetailedText(details)
246 mbox.exec_()
249 def information(title, message=None, details=None, informative_text=None):
250 """Show information with the provided title and message."""
251 if message is None:
252 message = title
253 mbox = QtGui.QMessageBox(active_window())
254 mbox.setStandardButtons(QtGui.QMessageBox.Close)
255 mbox.setDefaultButton(QtGui.QMessageBox.Close)
256 mbox.setWindowTitle(title)
257 mbox.setWindowModality(Qt.WindowModal)
258 mbox.setTextFormat(Qt.PlainText)
259 mbox.setText(message)
260 if informative_text:
261 mbox.setInformativeText(informative_text)
262 if details:
263 mbox.setDetailedText(details)
264 # Render into a 1-inch wide pixmap
265 pixmap = icons.cola().pixmap(defs.large_icon)
266 mbox.setIconPixmap(pixmap)
267 mbox.exec_()
270 def question(title, msg, default=True):
271 """Launches a QMessageBox question with the provided title and message.
272 Passing "default=False" will make "No" the default choice."""
273 yes = QtGui.QMessageBox.Yes
274 no = QtGui.QMessageBox.No
275 buttons = yes | no
276 if default:
277 default = yes
278 else:
279 default = no
280 result = (QtGui.QMessageBox
281 .question(active_window(), title, msg, buttons, default))
282 return result == QtGui.QMessageBox.Yes
285 def tree_selection(tree_item, items):
286 """Returns an array of model items that correspond to the selected
287 QTreeWidgetItem children"""
288 selected = []
289 count = min(tree_item.childCount(), len(items))
290 for idx in range(count):
291 if tree_item.child(idx).isSelected():
292 selected.append(items[idx])
294 return selected
297 def tree_selection_items(tree_item):
298 """Returns selected widget items"""
299 selected = []
300 for idx in range(tree_item.childCount()):
301 child = tree_item.child(idx)
302 if child.isSelected():
303 selected.append(child)
305 return selected
308 def selected_item(list_widget, items):
309 """Returns the model item that corresponds to the selected QListWidget
310 row."""
311 widget_items = list_widget.selectedItems()
312 if not widget_items:
313 return None
314 widget_item = widget_items[0]
315 row = list_widget.row(widget_item)
316 if row < len(items):
317 return items[row]
318 else:
319 return None
322 def selected_items(list_widget, items):
323 """Returns an array of model items that correspond to the selected
324 QListWidget rows."""
325 item_count = len(items)
326 selected = []
327 for widget_item in list_widget.selectedItems():
328 row = list_widget.row(widget_item)
329 if row < item_count:
330 selected.append(items[row])
331 return selected
334 def open_file(title, directory=None):
335 """Creates an Open File dialog and returns a filename."""
336 return ustr(QtGui.QFileDialog
337 .getOpenFileName(active_window(), title, directory))
340 def open_files(title, directory=None, filter=None):
341 """Creates an Open File dialog and returns a list of filenames."""
342 return (QtGui.QFileDialog
343 .getOpenFileNames(active_window(), title, directory, filter))
346 def opendir_dialog(title, path):
347 """Prompts for a directory path"""
349 flags = (QtGui.QFileDialog.ShowDirsOnly |
350 QtGui.QFileDialog.DontResolveSymlinks)
351 return ustr(QtGui.QFileDialog
352 .getExistingDirectory(active_window(),
353 title, path, flags))
356 def save_as(filename, title='Save As...'):
357 """Creates a Save File dialog and returns a filename."""
358 return ustr(QtGui.QFileDialog
359 .getSaveFileName(active_window(), title, filename))
362 def copy_path(filename, absolute=True):
363 """Copy a filename to the clipboard"""
364 if filename is None:
365 return
366 if absolute:
367 filename = core.abspath(filename)
368 set_clipboard(filename)
371 def set_clipboard(text):
372 """Sets the copy/paste buffer to text."""
373 if not text:
374 return
375 clipboard = QtGui.QApplication.clipboard()
376 clipboard.setText(text, QtGui.QClipboard.Clipboard)
377 clipboard.setText(text, QtGui.QClipboard.Selection)
378 persist_clipboard()
381 def persist_clipboard():
382 """Persist the clipboard
384 X11 stores only a reference to the clipboard data.
385 Send a clipboard event to force a copy of the clipboard to occur.
386 This ensures that the clipboard is present after git-cola exits.
387 Otherwise, the reference is destroyed on exit.
389 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
392 clipboard = QtGui.QApplication.clipboard()
393 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
394 QtGui.QApplication.sendEvent(clipboard, event)
397 def add_action_bool(widget, text, fn, checked, *shortcuts):
398 tip = text
399 action = _add_action(widget, text, tip, fn, connect_action_bool, *shortcuts)
400 action.setCheckable(True)
401 action.setChecked(checked)
402 return action
405 def add_action(widget, text, fn, *shortcuts):
406 tip = text
407 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
410 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
411 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
414 def _add_action(widget, text, tip, fn, connect, *shortcuts):
415 action = QtGui.QAction(text, widget)
416 if tip:
417 action.setStatusTip(tip)
418 connect(action, fn)
419 if shortcuts:
420 action.setShortcuts(shortcuts)
421 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
422 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
423 widget.addAction(action)
424 return action
427 def set_selected_item(widget, idx):
428 """Sets a the currently selected item to the item at index idx."""
429 if type(widget) is QtGui.QTreeWidget:
430 item = widget.topLevelItem(idx)
431 if item:
432 widget.setItemSelected(item, True)
433 widget.setCurrentItem(item)
436 def add_items(widget, items):
437 """Adds items to a widget."""
438 for item in items:
439 widget.addItem(item)
442 def set_items(widget, items):
443 """Clear the existing widget contents and set the new items."""
444 widget.clear()
445 add_items(widget, items)
449 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
450 """Given a filename, return a TreeWidgetItem for a status widget
452 "staged", "deleted, and "untracked" control which icon is used.
455 icon_name = icons.status(filename, deleted, staged, untracked)
456 return TreeWidgetItem(filename, resources.icon(icon_name), deleted=deleted)
459 def add_close_action(widget):
460 """Adds close action and shortcuts to a widget."""
461 return add_action(widget, N_('Close...'),
462 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
465 def center_on_screen(widget):
466 """Move widget to the center of the default screen"""
467 desktop = QtGui.QApplication.instance().desktop()
468 rect = desktop.screenGeometry(QtGui.QCursor().pos())
469 cy = rect.height()//2
470 cx = rect.width()//2
471 widget.move(cx - widget.width()//2, cy - widget.height()//2)
474 def default_size(parent, width, height):
475 """Return the parent's size, or the provided defaults"""
476 if parent is not None:
477 width = parent.width()
478 height = parent.height()
479 return (width, height)
482 def default_monospace_font():
483 font = QtGui.QFont()
484 family = 'Monospace'
485 if utils.is_darwin():
486 family = 'Monaco'
487 font.setFamily(family)
488 return font
491 def diff_font_str():
492 font_str = gitcfg.current().get(prefs.FONTDIFF)
493 if font_str is None:
494 font = default_monospace_font()
495 font_str = ustr(font.toString())
496 return font_str
499 def diff_font():
500 return font(diff_font_str())
503 def font(string):
504 font = QtGui.QFont()
505 font.fromString(string)
506 return font
509 def create_button(text='', layout=None, tooltip=None, icon=None,
510 enabled=True, default=False):
511 """Create a button, set its title, and add it to the parent."""
512 button = QtGui.QPushButton()
513 button.setCursor(Qt.PointingHandCursor)
514 if text:
515 button.setText(text)
516 if icon is not None:
517 button.setIcon(icon)
518 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
519 if tooltip is not None:
520 button.setToolTip(tooltip)
521 if layout is not None:
522 layout.addWidget(button)
523 if not enabled:
524 button.setEnabled(False)
525 if default:
526 button.setDefault(True)
527 return button
530 def create_action_button(tooltip=None, icon=None):
531 button = QtGui.QPushButton()
532 button.setCursor(Qt.PointingHandCursor)
533 button.setFlat(True)
534 if tooltip is not None:
535 button.setToolTip(tooltip)
536 if icon is not None:
537 button.setIcon(icon)
538 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
539 return button
542 def ok_button(text, default=False, enabled=True):
543 return create_button(text=text, icon=icons.ok(),
544 default=default, enabled=enabled)
547 def close_button():
548 return create_button(text=N_('Close'), icon=icons.close())
551 def edit_button(enabled=True, default=False):
552 return create_button(text=N_('Edit'), icon=icons.edit(),
553 enabled=enabled, default=default)
556 def refresh_button(enabled=True, default=False):
557 return create_button(text=N_('Refresh'), icon=icons.sync(),
558 enabled=enabled, default=default)
561 def hide_button_menu_indicator(button):
562 cls = type(button)
563 name = cls.__name__
564 stylesheet = """
565 %(name)s::menu-indicator {
566 image: none;
569 if name == 'QPushButton':
570 stylesheet += """
571 %(name)s {
572 border-style: none;
575 button.setStyleSheet(stylesheet % {'name': name})
578 def checkbox(text='', tooltip='', checked=None):
579 cb = QtGui.QCheckBox()
580 if text:
581 cb.setText(text)
582 if tooltip:
583 cb.setToolTip(tooltip)
584 if checked is not None:
585 cb.setChecked(checked)
587 url = icons.check_name()
588 style = """
589 QCheckBox::indicator {
590 width: %(size)dpx;
591 height: %(size)dpx;
593 QCheckBox::indicator::unchecked {
594 border: %(border)dpx solid #999;
595 background: #fff;
597 QCheckBox::indicator::checked {
598 image: url(%(url)s);
599 border: %(border)dpx solid black;
600 background: #fff;
602 """ % dict(size=defs.checkbox, border=defs.border, url=url)
603 cb.setStyleSheet(style)
605 return cb
608 def radio(text='', tooltip='', checked=None):
609 rb = QtGui.QRadioButton()
610 if text:
611 rb.setText(text)
612 if tooltip:
613 rb.setToolTip(tooltip)
614 if checked is not None:
615 rb.setChecked(checked)
617 size = defs.checkbox
618 radius = size / 2
619 border = defs.radio_border
620 url = icons.dot_name()
621 style = """
622 QRadioButton::indicator {
623 width: %(size)dpx;
624 height: %(size)dpx;
626 QRadioButton::indicator::unchecked {
627 background: #fff;
628 border: %(border)dpx solid #999;
629 border-radius: %(radius)dpx;
631 QRadioButton::indicator::checked {
632 image: url(%(url)s);
633 background: #fff;
634 border: %(border)dpx solid black;
635 border-radius: %(radius)dpx;
637 """ % dict(size=size, radius=radius, border=border, url=url)
638 rb.setStyleSheet(style)
640 return rb
643 class DockTitleBarWidget(QtGui.QWidget):
645 def __init__(self, parent, title, stretch=True):
646 QtGui.QWidget.__init__(self, parent)
647 self.label = label = QtGui.QLabel()
648 font = label.font()
649 font.setBold(True)
650 label.setFont(font)
651 label.setText(title)
652 label.setCursor(Qt.OpenHandCursor)
654 self.close_button = create_action_button(
655 tooltip=N_('Close'), icon=icons.close())
657 self.toggle_button = create_action_button(
658 tooltip=N_('Detach'), icon=icons.external())
660 self.corner_layout = hbox(defs.no_margin, defs.spacing)
662 if stretch:
663 separator = STRETCH
664 else:
665 separator = SKIPPED
667 self.main_layout = hbox(defs.small_margin, defs.spacing,
668 label, separator, self.corner_layout,
669 self.toggle_button, self.close_button)
670 self.setLayout(self.main_layout)
672 connect_button(self.toggle_button, self.toggle_floating)
673 connect_button(self.close_button, self.toggle_visibility)
675 def toggle_floating(self):
676 self.parent().setFloating(not self.parent().isFloating())
677 self.update_tooltips()
679 def toggle_visibility(self):
680 self.parent().toggleViewAction().trigger()
682 def set_title(self, title):
683 self.label.setText(title)
685 def add_corner_widget(self, widget):
686 self.corner_layout.addWidget(widget)
688 def update_tooltips(self):
689 if self.parent().isFloating():
690 tooltip = N_('Attach')
691 else:
692 tooltip = N_('Detach')
693 self.toggle_button.setToolTip(tooltip)
696 def create_dock(title, parent, stretch=True):
697 """Create a dock widget and set it up accordingly."""
698 dock = QtGui.QDockWidget(parent)
699 dock.setWindowTitle(title)
700 dock.setObjectName(title)
701 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
702 dock.setTitleBarWidget(titlebar)
703 if hasattr(parent, 'dockwidgets'):
704 parent.dockwidgets.append(dock)
705 return dock
708 def create_menu(title, parent):
709 """Create a menu and set its title."""
710 qmenu = QtGui.QMenu(parent)
711 qmenu.setTitle(title)
712 return qmenu
715 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
716 button = QtGui.QToolButton()
717 button.setAutoRaise(True)
718 button.setAutoFillBackground(True)
719 button.setCursor(Qt.PointingHandCursor)
720 if icon is not None:
721 button.setIcon(icon)
722 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
723 if text is not None:
724 button.setText(text)
725 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
726 if tooltip is not None:
727 button.setToolTip(tooltip)
728 if layout is not None:
729 layout.addWidget(button)
730 return button
733 def mimedata_from_paths(paths):
734 """Return mimedata with a list of absolute path URLs"""
736 abspaths = [core.abspath(path) for path in paths]
737 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
739 mimedata = QtCore.QMimeData()
740 mimedata.setUrls(urls)
742 # The text/x-moz-list format is always included by Qt, and doing
743 # mimedata.removeFormat('text/x-moz-url') has no effect.
744 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
746 # gnome-terminal expects utf-16 encoded text, but other terminals,
747 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
748 # to override the default.
749 paths_text = subprocess.list2cmdline(abspaths)
750 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
751 moz_text = core.encode(paths_text, encoding=encoding)
752 mimedata.setData('text/x-moz-url', moz_text)
754 return mimedata
757 def path_mimetypes():
758 return ['text/uri-list', 'text/x-moz-url']
761 class BlockSignals(object):
762 """Context manager for blocking a signals on a widget"""
764 def __init__(self, *widgets):
765 self.widgets = widgets
766 self.values = {}
768 def __enter__(self):
769 for w in self.widgets:
770 self.values[w] = w.blockSignals(True)
771 return self
773 def __exit__(self, exc_type, exc_val, exc_tb):
774 for w in self.widgets:
775 w.blockSignals(self.values[w])
778 class Task(QtCore.QRunnable):
779 """Disable auto-deletion to avoid gc issues
781 Python's garbage collector will try to double-free the task
782 once it's finished, so disable Qt's auto-deletion as a workaround.
786 FINISHED = SIGNAL('TASK_FINISHED')
787 RESULT = SIGNAL('TASK_RESULT')
789 def __init__(self, parent, *args, **kwargs):
790 QtCore.QRunnable.__init__(self)
792 self.channel = QtCore.QObject(parent)
793 self.result = None
794 self.setAutoDelete(False)
796 def run(self):
797 self.result = self.task()
798 self.channel.emit(self.RESULT, self.result)
799 self.done()
801 def task(self):
802 pass
804 def done(self):
805 self.channel.emit(self.FINISHED, self)
807 def connect(self, handler):
808 self.channel.connect(self.channel, self.RESULT,
809 handler, Qt.QueuedConnection)
812 class SimpleTask(Task):
813 """Run a simple callable as a task"""
815 def __init__(self, parent, fn, *args, **kwargs):
816 Task.__init__(self, parent)
818 self.fn = fn
819 self.args = args
820 self.kwargs = kwargs
822 def task(self):
823 return self.fn(*self.args, **self.kwargs)
826 class RunTask(QtCore.QObject):
827 """Runs QRunnable instances and transfers control when they finish"""
829 def __init__(self, parent=None):
830 QtCore.QObject.__init__(self, parent)
831 self.tasks = []
832 self.task_details = {}
833 self.threadpool = QtCore.QThreadPool.globalInstance()
835 def start(self, task, progress=None, finish=None):
836 """Start the task and register a callback"""
837 if progress is not None:
838 progress.show()
839 # prevents garbage collection bugs in certain PyQt4 versions
840 self.tasks.append(task)
841 task_id = id(task)
842 self.task_details[task_id] = (progress, finish)
844 self.connect(task.channel, Task.FINISHED, self.finish,
845 Qt.QueuedConnection)
846 self.threadpool.start(task)
848 def finish(self, task, *args, **kwargs):
849 task_id = id(task)
850 try:
851 self.tasks.remove(task)
852 except:
853 pass
854 try:
855 progress, finish = self.task_details[task_id]
856 del self.task_details[task_id]
857 except KeyError:
858 finish = progress = None
860 if progress is not None:
861 progress.hide()
863 if finish is not None:
864 finish(task, *args, **kwargs)
867 # Syntax highlighting
869 def rgba(r, g, b, a=255):
870 c = QtGui.QColor()
871 c.setRgb(r, g, b)
872 c.setAlpha(a)
873 return c
876 def RGB(args):
877 return rgba(*args)
880 def make_format(fg=None, bg=None, bold=False):
881 fmt = QtGui.QTextCharFormat()
882 if fg:
883 fmt.setForeground(fg)
884 if bg:
885 fmt.setBackground(bg)
886 if bold:
887 fmt.setFontWeight(QtGui.QFont.Bold)
888 return fmt
891 def install():
892 Interaction.critical = staticmethod(critical)
893 Interaction.confirm = staticmethod(confirm)
894 Interaction.question = staticmethod(question)
895 Interaction.information = staticmethod(information)