qtutils: avoid None in add_items()
[git-cola.git] / cola / qtutils.py
blobfdc05515a5d16ca7300d91ef3c07ecb1f98b7a82
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.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)
51 def active_window():
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 STRETCH = object()
64 SKIPPED = object()
67 def box(cls, margin, spacing, *items):
68 stretch = STRETCH
69 skipped = SKIPPED
70 layout = cls()
71 layout.setMargin(margin)
72 layout.setSpacing(spacing)
74 if PY3:
75 int_types = (int,)
76 else:
77 int_types = (int, long)
79 for i in items:
80 if isinstance(i, QtGui.QWidget):
81 layout.addWidget(i)
82 elif isinstance(i, (QtGui.QHBoxLayout, QtGui.QVBoxLayout,
83 QtGui.QFormLayout, QtGui.QLayout)):
84 layout.addLayout(i)
85 elif i is stretch:
86 layout.addStretch()
87 elif i is skipped:
88 continue
89 elif isinstance(i, int_types):
90 layout.addSpacing(i)
92 return layout
95 def form(margin, spacing, *widgets):
96 layout = QtGui.QFormLayout()
97 layout.setMargin(margin)
98 layout.setSpacing(spacing)
99 layout.setFieldGrowthPolicy(QtGui.QFormLayout.ExpandingFieldsGrow)
101 for idx, (label, widget) in enumerate(widgets):
102 if isinstance(label, (str, ustr)):
103 layout.addRow(label, widget)
104 else:
105 layout.setWidget(idx, QtGui.QFormLayout.LabelRole, label)
106 layout.setWidget(idx, QtGui.QFormLayout.FieldRole, widget)
108 return layout
111 def grid(margin, spacing, *widgets):
112 layout = QtGui.QGridLayout()
113 layout.setMargin(defs.no_margin)
114 layout.setSpacing(defs.spacing)
116 for row in widgets:
117 item = row[0]
118 if isinstance(item, QtGui.QWidget):
119 layout.addWidget(*row)
120 elif isinstance(item, QtGui.QLayoutItem):
121 layout.addItem(*row)
123 return layout
126 def splitter(orientation, *widgets):
127 layout = QtGui.QSplitter()
128 layout.setOrientation(orientation)
129 layout.setHandleWidth(defs.handle_width)
130 layout.setChildrenCollapsible(True)
131 for idx, widget in enumerate(widgets):
132 layout.addWidget(widget)
133 layout.setStretchFactor(idx, 1)
135 return layout
138 def prompt(msg, title=None, text=''):
139 """Presents the user with an input widget and returns the input."""
140 if title is None:
141 title = msg
142 result = QtGui.QInputDialog.getText(active_window(), msg, title,
143 QtGui.QLineEdit.Normal, text)
144 return (ustr(result[0]), result[1])
147 def create_listwidget_item(text, filename):
148 """Creates a QListWidgetItem with text and the icon at filename."""
149 item = QtGui.QListWidgetItem()
150 item.setIcon(QtGui.QIcon(filename))
151 item.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
152 item.setText(text)
153 return item
156 class TreeWidgetItem(QtGui.QTreeWidgetItem):
158 TYPE = QtGui.QStandardItem.UserType + 101
160 def __init__(self, path, icon, deleted):
161 QtGui.QTreeWidgetItem.__init__(self)
162 self.path = path
163 self.deleted = deleted
164 self.setIcon(0, icons.from_name(icon))
165 self.setText(0, path)
167 def type(self):
168 return self.TYPE
171 def paths_from_indexes(model, indexes,
172 item_type=TreeWidgetItem.TYPE,
173 item_filter=None):
174 """Return paths from a list of QStandardItemModel indexes"""
175 items = [model.itemFromIndex(i) for i in indexes]
176 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
179 def paths_from_items(items,
180 item_type=TreeWidgetItem.TYPE,
181 item_filter=None):
182 """Return a list of paths from a list of items"""
183 if item_filter is None:
184 item_filter = lambda x: True
185 return [i.path for i in items
186 if i.type() == item_type and item_filter(i)]
189 def confirm(title, text, informative_text, ok_text,
190 icon=None, default=True,
191 cancel_text=None, cancel_icon=None):
192 """Confirm that an action should take place"""
193 msgbox = QtGui.QMessageBox(active_window())
194 msgbox.setWindowModality(Qt.WindowModal)
195 msgbox.setWindowTitle(title)
196 msgbox.setText(text)
197 msgbox.setInformativeText(informative_text)
199 icon = icons.mkicon(icon, icons.ok)
200 ok = msgbox.addButton(ok_text, QtGui.QMessageBox.ActionRole)
201 ok.setIcon(icon)
203 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
204 cancel_icon = icons.mkicon(cancel_icon, icons.close)
205 cancel.setIcon(cancel_icon)
206 if cancel_text:
207 cancel.setText(cancel_text)
209 if default:
210 msgbox.setDefaultButton(ok)
211 else:
212 msgbox.setDefaultButton(cancel)
213 msgbox.exec_()
214 return msgbox.clickedButton() == ok
217 class ResizeableMessageBox(QtGui.QMessageBox):
219 def __init__(self, parent):
220 QtGui.QMessageBox.__init__(self, parent)
221 self.setMouseTracking(True)
222 self.setSizeGripEnabled(True)
224 def event(self, event):
225 res = QtGui.QMessageBox.event(self, event)
226 event_type = event.type()
227 if (event_type == QtCore.QEvent.MouseMove or
228 event_type == QtCore.QEvent.MouseButtonPress):
229 maxi = QtCore.QSize(defs.max_size, defs.max_size)
230 self.setMaximumSize(maxi)
231 text = self.findChild(QtGui.QTextEdit)
232 if text is not None:
233 expand = QtGui.QSizePolicy.Expanding
234 text.setSizePolicy(QtGui.QSizePolicy(expand, expand))
235 text.setMaximumSize(maxi)
236 return res
239 def critical(title, message=None, details=None):
240 """Show a warning with the provided title and message."""
241 if message is None:
242 message = title
243 mbox = ResizeableMessageBox(active_window())
244 mbox.setWindowTitle(title)
245 mbox.setTextFormat(Qt.PlainText)
246 mbox.setText(message)
247 mbox.setIcon(QtGui.QMessageBox.Critical)
248 mbox.setStandardButtons(QtGui.QMessageBox.Close)
249 mbox.setDefaultButton(QtGui.QMessageBox.Close)
250 if details:
251 mbox.setDetailedText(details)
252 mbox.exec_()
255 def information(title, message=None, details=None, informative_text=None):
256 """Show information with the provided title and message."""
257 if message is None:
258 message = title
259 mbox = QtGui.QMessageBox(active_window())
260 mbox.setStandardButtons(QtGui.QMessageBox.Close)
261 mbox.setDefaultButton(QtGui.QMessageBox.Close)
262 mbox.setWindowTitle(title)
263 mbox.setWindowModality(Qt.WindowModal)
264 mbox.setTextFormat(Qt.PlainText)
265 mbox.setText(message)
266 if informative_text:
267 mbox.setInformativeText(informative_text)
268 if details:
269 mbox.setDetailedText(details)
270 # Render into a 1-inch wide pixmap
271 pixmap = icons.cola().pixmap(defs.large_icon)
272 mbox.setIconPixmap(pixmap)
273 mbox.exec_()
276 def question(title, msg, default=True):
277 """Launches a QMessageBox question with the provided title and message.
278 Passing "default=False" will make "No" the default choice."""
279 yes = QtGui.QMessageBox.Yes
280 no = QtGui.QMessageBox.No
281 buttons = yes | no
282 if default:
283 default = yes
284 else:
285 default = no
286 result = (QtGui.QMessageBox
287 .question(active_window(), title, msg, buttons, default))
288 return result == QtGui.QMessageBox.Yes
291 def tree_selection(tree_item, items):
292 """Returns an array of model items that correspond to the selected
293 QTreeWidgetItem children"""
294 selected = []
295 count = min(tree_item.childCount(), len(items))
296 for idx in range(count):
297 if tree_item.child(idx).isSelected():
298 selected.append(items[idx])
300 return selected
303 def tree_selection_items(tree_item):
304 """Returns selected widget items"""
305 selected = []
306 for idx in range(tree_item.childCount()):
307 child = tree_item.child(idx)
308 if child.isSelected():
309 selected.append(child)
311 return selected
314 def selected_item(list_widget, items):
315 """Returns the model item that corresponds to the selected QListWidget
316 row."""
317 widget_items = list_widget.selectedItems()
318 if not widget_items:
319 return None
320 widget_item = widget_items[0]
321 row = list_widget.row(widget_item)
322 if row < len(items):
323 return items[row]
324 else:
325 return None
328 def selected_items(list_widget, items):
329 """Returns an array of model items that correspond to the selected
330 QListWidget rows."""
331 item_count = len(items)
332 selected = []
333 for widget_item in list_widget.selectedItems():
334 row = list_widget.row(widget_item)
335 if row < item_count:
336 selected.append(items[row])
337 return selected
340 def open_file(title, directory=None):
341 """Creates an Open File dialog and returns a filename."""
342 return ustr(QtGui.QFileDialog
343 .getOpenFileName(active_window(), title, directory))
346 def open_files(title, directory=None, filter=None):
347 """Creates an Open File dialog and returns a list of filenames."""
348 return (QtGui.QFileDialog
349 .getOpenFileNames(active_window(), title, directory, filter))
352 def opendir_dialog(title, path):
353 """Prompts for a directory path"""
355 flags = (QtGui.QFileDialog.ShowDirsOnly |
356 QtGui.QFileDialog.DontResolveSymlinks)
357 return ustr(QtGui.QFileDialog
358 .getExistingDirectory(active_window(),
359 title, path, flags))
362 def save_as(filename, title='Save As...'):
363 """Creates a Save File dialog and returns a filename."""
364 return ustr(QtGui.QFileDialog
365 .getSaveFileName(active_window(), title, filename))
368 def copy_path(filename, absolute=True):
369 """Copy a filename to the clipboard"""
370 if filename is None:
371 return
372 if absolute:
373 filename = core.abspath(filename)
374 set_clipboard(filename)
377 def set_clipboard(text):
378 """Sets the copy/paste buffer to text."""
379 if not text:
380 return
381 clipboard = QtGui.QApplication.clipboard()
382 clipboard.setText(text, QtGui.QClipboard.Clipboard)
383 clipboard.setText(text, QtGui.QClipboard.Selection)
384 persist_clipboard()
387 def persist_clipboard():
388 """Persist the clipboard
390 X11 stores only a reference to the clipboard data.
391 Send a clipboard event to force a copy of the clipboard to occur.
392 This ensures that the clipboard is present after git-cola exits.
393 Otherwise, the reference is destroyed on exit.
395 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
398 clipboard = QtGui.QApplication.clipboard()
399 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
400 QtGui.QApplication.sendEvent(clipboard, event)
403 def add_action_bool(widget, text, fn, checked, *shortcuts):
404 tip = text
405 action = _add_action(widget, text, tip, fn, connect_action_bool, *shortcuts)
406 action.setCheckable(True)
407 action.setChecked(checked)
408 return action
411 def add_action(widget, text, fn, *shortcuts):
412 tip = text
413 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
416 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
417 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
420 def _add_action(widget, text, tip, fn, connect, *shortcuts):
421 action = QtGui.QAction(text, widget)
422 if tip:
423 action.setStatusTip(tip)
424 connect(action, fn)
425 if shortcuts:
426 action.setShortcuts(shortcuts)
427 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
428 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
429 widget.addAction(action)
430 return action
433 def set_selected_item(widget, idx):
434 """Sets a the currently selected item to the item at index idx."""
435 if type(widget) is QtGui.QTreeWidget:
436 item = widget.topLevelItem(idx)
437 if item:
438 widget.setItemSelected(item, True)
439 widget.setCurrentItem(item)
442 def add_items(widget, items):
443 """Adds items to a widget."""
444 for item in items:
445 if item is None:
446 continue
447 widget.addItem(item)
450 def set_items(widget, items):
451 """Clear the existing widget contents and set the new items."""
452 widget.clear()
453 add_items(widget, items)
457 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
458 """Given a filename, return a TreeWidgetItem for a status widget
460 "staged", "deleted, and "untracked" control which icon is used.
463 icon_name = icons.status(filename, deleted, staged, untracked)
464 return TreeWidgetItem(filename, resources.icon(icon_name), deleted=deleted)
467 def add_close_action(widget):
468 """Adds close action and shortcuts to a widget."""
469 return add_action(widget, N_('Close...'),
470 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
473 def center_on_screen(widget):
474 """Move widget to the center of the default screen"""
475 desktop = QtGui.QApplication.instance().desktop()
476 rect = desktop.screenGeometry(QtGui.QCursor().pos())
477 cy = rect.height()//2
478 cx = rect.width()//2
479 widget.move(cx - widget.width()//2, cy - widget.height()//2)
482 def default_size(parent, width, height):
483 """Return the parent's size, or the provided defaults"""
484 if parent is not None:
485 width = parent.width()
486 height = parent.height()
487 return (width, height)
490 def default_monospace_font():
491 font = QtGui.QFont()
492 family = 'Monospace'
493 if utils.is_darwin():
494 family = 'Monaco'
495 font.setFamily(family)
496 return font
499 def diff_font_str():
500 font_str = gitcfg.current().get(prefs.FONTDIFF)
501 if font_str is None:
502 font = default_monospace_font()
503 font_str = ustr(font.toString())
504 return font_str
507 def diff_font():
508 return font(diff_font_str())
511 def font(string):
512 font = QtGui.QFont()
513 font.fromString(string)
514 return font
517 def create_button(text='', layout=None, tooltip=None, icon=None,
518 enabled=True, default=False):
519 """Create a button, set its title, and add it to the parent."""
520 button = QtGui.QPushButton()
521 button.setCursor(Qt.PointingHandCursor)
522 if text:
523 button.setText(text)
524 if icon is not None:
525 button.setIcon(icon)
526 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
527 if tooltip is not None:
528 button.setToolTip(tooltip)
529 if layout is not None:
530 layout.addWidget(button)
531 if not enabled:
532 button.setEnabled(False)
533 if default:
534 button.setDefault(True)
535 return button
538 def create_action_button(tooltip=None, icon=None):
539 button = QtGui.QPushButton()
540 button.setCursor(Qt.PointingHandCursor)
541 button.setFlat(True)
542 if tooltip is not None:
543 button.setToolTip(tooltip)
544 if icon is not None:
545 button.setIcon(icon)
546 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
547 return button
550 def ok_button(text, default=False, enabled=True):
551 return create_button(text=text, icon=icons.ok(),
552 default=default, enabled=enabled)
555 def close_button():
556 return create_button(text=N_('Close'), icon=icons.close())
559 def edit_button(enabled=True, default=False):
560 return create_button(text=N_('Edit'), icon=icons.edit(),
561 enabled=enabled, default=default)
564 def refresh_button(enabled=True, default=False):
565 return create_button(text=N_('Refresh'), icon=icons.sync(),
566 enabled=enabled, default=default)
569 def hide_button_menu_indicator(button):
570 cls = type(button)
571 name = cls.__name__
572 stylesheet = """
573 %(name)s::menu-indicator {
574 image: none;
577 if name == 'QPushButton':
578 stylesheet += """
579 %(name)s {
580 border-style: none;
583 button.setStyleSheet(stylesheet % {'name': name})
586 def checkbox(text='', tooltip='', checked=None):
587 cb = QtGui.QCheckBox()
588 if text:
589 cb.setText(text)
590 if tooltip:
591 cb.setToolTip(tooltip)
592 if checked is not None:
593 cb.setChecked(checked)
595 url = icons.check_name()
596 style = """
597 QCheckBox::indicator {
598 width: %(size)dpx;
599 height: %(size)dpx;
601 QCheckBox::indicator::unchecked {
602 border: %(border)dpx solid #999;
603 background: #fff;
605 QCheckBox::indicator::checked {
606 image: url(%(url)s);
607 border: %(border)dpx solid black;
608 background: #fff;
610 """ % dict(size=defs.checkbox, border=defs.border, url=url)
611 cb.setStyleSheet(style)
613 return cb
616 def radio(text='', tooltip='', checked=None):
617 rb = QtGui.QRadioButton()
618 if text:
619 rb.setText(text)
620 if tooltip:
621 rb.setToolTip(tooltip)
622 if checked is not None:
623 rb.setChecked(checked)
625 size = defs.checkbox
626 radius = size / 2
627 border = defs.radio_border
628 url = icons.dot_name()
629 style = """
630 QRadioButton::indicator {
631 width: %(size)dpx;
632 height: %(size)dpx;
634 QRadioButton::indicator::unchecked {
635 background: #fff;
636 border: %(border)dpx solid #999;
637 border-radius: %(radius)dpx;
639 QRadioButton::indicator::checked {
640 image: url(%(url)s);
641 background: #fff;
642 border: %(border)dpx solid black;
643 border-radius: %(radius)dpx;
645 """ % dict(size=size, radius=radius, border=border, url=url)
646 rb.setStyleSheet(style)
648 return rb
651 class DockTitleBarWidget(QtGui.QWidget):
653 def __init__(self, parent, title, stretch=True):
654 QtGui.QWidget.__init__(self, parent)
655 self.label = label = QtGui.QLabel()
656 font = label.font()
657 font.setBold(True)
658 label.setFont(font)
659 label.setText(title)
660 label.setCursor(Qt.OpenHandCursor)
662 self.close_button = create_action_button(
663 tooltip=N_('Close'), icon=icons.close())
665 self.toggle_button = create_action_button(
666 tooltip=N_('Detach'), icon=icons.external())
668 self.corner_layout = hbox(defs.no_margin, defs.spacing)
670 if stretch:
671 separator = STRETCH
672 else:
673 separator = SKIPPED
675 self.main_layout = hbox(defs.small_margin, defs.spacing,
676 label, separator, self.corner_layout,
677 self.toggle_button, self.close_button)
678 self.setLayout(self.main_layout)
680 connect_button(self.toggle_button, self.toggle_floating)
681 connect_button(self.close_button, self.toggle_visibility)
683 def toggle_floating(self):
684 self.parent().setFloating(not self.parent().isFloating())
685 self.update_tooltips()
687 def toggle_visibility(self):
688 self.parent().toggleViewAction().trigger()
690 def set_title(self, title):
691 self.label.setText(title)
693 def add_corner_widget(self, widget):
694 self.corner_layout.addWidget(widget)
696 def update_tooltips(self):
697 if self.parent().isFloating():
698 tooltip = N_('Attach')
699 else:
700 tooltip = N_('Detach')
701 self.toggle_button.setToolTip(tooltip)
704 def create_dock(title, parent, stretch=True):
705 """Create a dock widget and set it up accordingly."""
706 dock = QtGui.QDockWidget(parent)
707 dock.setWindowTitle(title)
708 dock.setObjectName(title)
709 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
710 dock.setTitleBarWidget(titlebar)
711 if hasattr(parent, 'dockwidgets'):
712 parent.dockwidgets.append(dock)
713 return dock
716 def create_menu(title, parent):
717 """Create a menu and set its title."""
718 qmenu = QtGui.QMenu(parent)
719 qmenu.setTitle(title)
720 return qmenu
723 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
724 button = QtGui.QToolButton()
725 button.setAutoRaise(True)
726 button.setAutoFillBackground(True)
727 button.setCursor(Qt.PointingHandCursor)
728 if icon is not None:
729 button.setIcon(icon)
730 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
731 if text is not None:
732 button.setText(text)
733 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
734 if tooltip is not None:
735 button.setToolTip(tooltip)
736 if layout is not None:
737 layout.addWidget(button)
738 return button
741 def mimedata_from_paths(paths):
742 """Return mimedata with a list of absolute path URLs"""
744 abspaths = [core.abspath(path) for path in paths]
745 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
747 mimedata = QtCore.QMimeData()
748 mimedata.setUrls(urls)
750 # The text/x-moz-list format is always included by Qt, and doing
751 # mimedata.removeFormat('text/x-moz-url') has no effect.
752 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
754 # gnome-terminal expects utf-16 encoded text, but other terminals,
755 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
756 # to override the default.
757 paths_text = subprocess.list2cmdline(abspaths)
758 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
759 moz_text = core.encode(paths_text, encoding=encoding)
760 mimedata.setData('text/x-moz-url', moz_text)
762 return mimedata
765 def path_mimetypes():
766 return ['text/uri-list', 'text/x-moz-url']
769 class BlockSignals(object):
770 """Context manager for blocking a signals on a widget"""
772 def __init__(self, *widgets):
773 self.widgets = widgets
774 self.values = {}
776 def __enter__(self):
777 for w in self.widgets:
778 self.values[w] = w.blockSignals(True)
779 return self
781 def __exit__(self, exc_type, exc_val, exc_tb):
782 for w in self.widgets:
783 w.blockSignals(self.values[w])
786 class Task(QtCore.QRunnable):
787 """Disable auto-deletion to avoid gc issues
789 Python's garbage collector will try to double-free the task
790 once it's finished, so disable Qt's auto-deletion as a workaround.
794 FINISHED = SIGNAL('TASK_FINISHED')
795 RESULT = SIGNAL('TASK_RESULT')
797 def __init__(self, parent, *args, **kwargs):
798 QtCore.QRunnable.__init__(self)
800 self.channel = QtCore.QObject(parent)
801 self.result = None
802 self.setAutoDelete(False)
804 def run(self):
805 self.result = self.task()
806 self.channel.emit(self.RESULT, self.result)
807 self.done()
809 def task(self):
810 pass
812 def done(self):
813 self.channel.emit(self.FINISHED, self)
815 def connect(self, handler):
816 self.channel.connect(self.channel, self.RESULT,
817 handler, Qt.QueuedConnection)
820 class SimpleTask(Task):
821 """Run a simple callable as a task"""
823 def __init__(self, parent, fn, *args, **kwargs):
824 Task.__init__(self, parent)
826 self.fn = fn
827 self.args = args
828 self.kwargs = kwargs
830 def task(self):
831 return self.fn(*self.args, **self.kwargs)
834 class RunTask(QtCore.QObject):
835 """Runs QRunnable instances and transfers control when they finish"""
837 def __init__(self, parent=None):
838 QtCore.QObject.__init__(self, parent)
839 self.tasks = []
840 self.task_details = {}
841 self.threadpool = QtCore.QThreadPool.globalInstance()
843 def start(self, task, progress=None, finish=None):
844 """Start the task and register a callback"""
845 if progress is not None:
846 progress.show()
847 # prevents garbage collection bugs in certain PyQt4 versions
848 self.tasks.append(task)
849 task_id = id(task)
850 self.task_details[task_id] = (progress, finish)
852 self.connect(task.channel, Task.FINISHED, self.finish,
853 Qt.QueuedConnection)
854 self.threadpool.start(task)
856 def finish(self, task, *args, **kwargs):
857 task_id = id(task)
858 try:
859 self.tasks.remove(task)
860 except:
861 pass
862 try:
863 progress, finish = self.task_details[task_id]
864 del self.task_details[task_id]
865 except KeyError:
866 finish = progress = None
868 if progress is not None:
869 progress.hide()
871 if finish is not None:
872 finish(task, *args, **kwargs)
875 # Syntax highlighting
877 def rgba(r, g, b, a=255):
878 c = QtGui.QColor()
879 c.setRgb(r, g, b)
880 c.setAlpha(a)
881 return c
884 def RGB(args):
885 return rgba(*args)
888 def make_format(fg=None, bg=None, bold=False):
889 fmt = QtGui.QTextCharFormat()
890 if fg:
891 fmt.setForeground(fg)
892 if bg:
893 fmt.setBackground(bg)
894 if bold:
895 fmt.setFontWeight(QtGui.QFont.Bold)
896 return fmt
899 def install():
900 Interaction.critical = staticmethod(critical)
901 Interaction.confirm = staticmethod(confirm)
902 Interaction.question = staticmethod(question)
903 Interaction.information = staticmethod(information)