setup: apply flake8 suggestions
[git-cola.git] / cola / qtutils.py
blob4a9aaa092420fa0d2927b88aa9633148da9f0eb3
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 from PyQt4 import QtGui
7 from PyQt4 import QtCore
8 from PyQt4.QtCore import Qt
9 from PyQt4.QtCore import SIGNAL
11 from cola import core
12 from cola import gitcfg
13 from cola import hotkeys
14 from cola import icons
15 from cola import utils
16 from cola.i18n import N_
17 from cola.interaction import Interaction
18 from cola.compat import int_types
19 from cola.compat import ustr
20 from cola.models import prefs
21 from cola.widgets import defs
24 def connect_action(action, fn):
25 """Connect an action to a function"""
26 action.connect(action, SIGNAL('triggered()'), fn)
29 def connect_action_bool(action, fn):
30 """Connect a triggered(bool) action to a function"""
31 action.connect(action, SIGNAL('triggered(bool)'), fn)
34 def connect_button(button, fn):
35 """Connect a button to a function"""
36 button.connect(button, SIGNAL('clicked()'), fn)
39 def button_action(button, action):
40 """Make a button trigger an action"""
41 connect_button(button, action.trigger)
44 def connect_toggle(toggle, fn):
45 toggle.connect(toggle, SIGNAL('toggled(bool)'), fn)
48 def active_window():
49 return QtGui.QApplication.activeWindow()
52 def hbox(margin, spacing, *items):
53 return box(QtGui.QHBoxLayout, margin, spacing, *items)
56 def vbox(margin, spacing, *items):
57 return box(QtGui.QVBoxLayout, margin, spacing, *items)
60 def buttongroup(*items):
61 group = QtGui.QButtonGroup()
62 for i in items:
63 group.addButton(i)
64 return group
67 STRETCH = object()
68 SKIPPED = object()
71 def box(cls, margin, spacing, *items):
72 stretch = STRETCH
73 skipped = SKIPPED
74 layout = cls()
75 layout.setMargin(margin)
76 layout.setSpacing(spacing)
78 for i in items:
79 if isinstance(i, QtGui.QWidget):
80 layout.addWidget(i)
81 elif isinstance(i, (QtGui.QHBoxLayout, QtGui.QVBoxLayout,
82 QtGui.QFormLayout, QtGui.QLayout)):
83 layout.addLayout(i)
84 elif i is stretch:
85 layout.addStretch()
86 elif i is skipped:
87 continue
88 elif isinstance(i, int_types):
89 layout.addSpacing(i)
91 return layout
94 def form(margin, spacing, *widgets):
95 layout = QtGui.QFormLayout()
96 layout.setMargin(margin)
97 layout.setSpacing(spacing)
98 layout.setFieldGrowthPolicy(QtGui.QFormLayout.ExpandingFieldsGrow)
100 for idx, (label, widget) in enumerate(widgets):
101 if isinstance(label, (str, ustr)):
102 layout.addRow(label, widget)
103 else:
104 layout.setWidget(idx, QtGui.QFormLayout.LabelRole, label)
105 layout.setWidget(idx, QtGui.QFormLayout.FieldRole, widget)
107 return layout
110 def grid(margin, spacing, *widgets):
111 layout = QtGui.QGridLayout()
112 layout.setMargin(defs.no_margin)
113 layout.setSpacing(defs.spacing)
115 for row in widgets:
116 item = row[0]
117 if isinstance(item, QtGui.QWidget):
118 layout.addWidget(*row)
119 elif isinstance(item, QtGui.QLayoutItem):
120 layout.addItem(*row)
122 return layout
125 def splitter(orientation, *widgets):
126 layout = QtGui.QSplitter()
127 layout.setOrientation(orientation)
128 layout.setHandleWidth(defs.handle_width)
129 layout.setChildrenCollapsible(True)
130 for idx, widget in enumerate(widgets):
131 layout.addWidget(widget)
132 layout.setStretchFactor(idx, 1)
134 return layout
137 def prompt(msg, title=None, text=''):
138 """Presents the user with an input widget and returns the input."""
139 if title is None:
140 title = msg
141 result = QtGui.QInputDialog.getText(active_window(), msg, title,
142 QtGui.QLineEdit.Normal, text)
143 return (result[0], result[1])
146 class TreeWidgetItem(QtGui.QTreeWidgetItem):
148 TYPE = QtGui.QStandardItem.UserType + 101
150 def __init__(self, path, icon, deleted):
151 QtGui.QTreeWidgetItem.__init__(self)
152 self.path = path
153 self.deleted = deleted
154 self.setIcon(0, icons.from_name(icon))
155 self.setText(0, path)
157 def type(self):
158 return self.TYPE
161 def paths_from_indexes(model, indexes,
162 item_type=TreeWidgetItem.TYPE,
163 item_filter=None):
164 """Return paths from a list of QStandardItemModel indexes"""
165 items = [model.itemFromIndex(i) for i in indexes]
166 return paths_from_items(items, item_type=item_type, item_filter=item_filter)
169 def _true_filter(x):
170 return True
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 = _true_filter
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 (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 (QtGui.QFileDialog
352 .getExistingDirectory(active_window(), title, path, flags))
355 def save_as(filename, title='Save As...'):
356 """Creates a Save File dialog and returns a filename."""
357 return (QtGui.QFileDialog
358 .getSaveFileName(active_window(), title, filename))
361 def copy_path(filename, absolute=True):
362 """Copy a filename to the clipboard"""
363 if filename is None:
364 return
365 if absolute:
366 filename = core.abspath(filename)
367 set_clipboard(filename)
370 def set_clipboard(text):
371 """Sets the copy/paste buffer to text."""
372 if not text:
373 return
374 clipboard = QtGui.QApplication.clipboard()
375 clipboard.setText(text, QtGui.QClipboard.Clipboard)
376 clipboard.setText(text, QtGui.QClipboard.Selection)
377 persist_clipboard()
380 def persist_clipboard():
381 """Persist the clipboard
383 X11 stores only a reference to the clipboard data.
384 Send a clipboard event to force a copy of the clipboard to occur.
385 This ensures that the clipboard is present after git-cola exits.
386 Otherwise, the reference is destroyed on exit.
388 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
391 clipboard = QtGui.QApplication.clipboard()
392 event = QtCore.QEvent(QtCore.QEvent.Clipboard)
393 QtGui.QApplication.sendEvent(clipboard, event)
396 def add_action_bool(widget, text, fn, checked, *shortcuts):
397 tip = text
398 action = _add_action(widget, text, tip, fn, connect_action_bool, *shortcuts)
399 action.setCheckable(True)
400 action.setChecked(checked)
401 return action
404 def add_action(widget, text, fn, *shortcuts):
405 tip = text
406 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
409 def add_action_with_status_tip(widget, text, tip, fn, *shortcuts):
410 return _add_action(widget, text, tip, fn, connect_action, *shortcuts)
413 def _add_action(widget, text, tip, fn, connect, *shortcuts):
414 action = QtGui.QAction(text, widget)
415 if hasattr(action, 'setIconVisibleInMenu'):
416 action.setIconVisibleInMenu(True)
417 if tip:
418 action.setStatusTip(tip)
419 connect(action, fn)
420 if shortcuts:
421 action.setShortcuts(shortcuts)
422 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
423 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
424 widget.addAction(action)
425 return action
428 def set_selected_item(widget, idx):
429 """Sets a the currently selected item to the item at index idx."""
430 if type(widget) is QtGui.QTreeWidget:
431 item = widget.topLevelItem(idx)
432 if item:
433 widget.setItemSelected(item, True)
434 widget.setCurrentItem(item)
437 def add_items(widget, items):
438 """Adds items to a widget."""
439 for item in items:
440 if item is None:
441 continue
442 widget.addItem(item)
445 def set_items(widget, items):
446 """Clear the existing widget contents and set the new items."""
447 widget.clear()
448 add_items(widget, items)
451 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
452 """Given a filename, return a TreeWidgetItem for a status widget
454 "staged", "deleted, and "untracked" control which icon is used.
457 icon_name = icons.status(filename, deleted, staged, untracked)
458 return TreeWidgetItem(filename, icons.name_from_basename(icon_name),
459 deleted=deleted)
462 def add_close_action(widget):
463 """Adds close action and shortcuts to a widget."""
464 return add_action(widget, N_('Close...'),
465 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
468 def center_on_screen(widget):
469 """Move widget to the center of the default screen"""
470 desktop = QtGui.QApplication.instance().desktop()
471 rect = desktop.screenGeometry(QtGui.QCursor().pos())
472 cy = rect.height()//2
473 cx = rect.width()//2
474 widget.move(cx - widget.width()//2, cy - widget.height()//2)
477 def default_size(parent, width, height):
478 """Return the parent's size, or the provided defaults"""
479 if parent is not None:
480 width = parent.width()
481 height = parent.height()
482 return (width, height)
485 def default_monospace_font():
486 font = QtGui.QFont()
487 family = 'Monospace'
488 if utils.is_darwin():
489 family = 'Monaco'
490 font.setFamily(family)
491 return font
494 def diff_font_str():
495 font_str = gitcfg.current().get(prefs.FONTDIFF)
496 if font_str is None:
497 font_str = default_monospace_font().toString()
498 return font_str
501 def diff_font():
502 return font(diff_font_str())
505 def font(string):
506 font = QtGui.QFont()
507 font.fromString(string)
508 return font
511 def create_button(text='', layout=None, tooltip=None, icon=None,
512 enabled=True, default=False):
513 """Create a button, set its title, and add it to the parent."""
514 button = QtGui.QPushButton()
515 button.setCursor(Qt.PointingHandCursor)
516 if text:
517 button.setText(text)
518 if icon is not None:
519 button.setIcon(icon)
520 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
521 if tooltip is not None:
522 button.setToolTip(tooltip)
523 if layout is not None:
524 layout.addWidget(button)
525 if not enabled:
526 button.setEnabled(False)
527 if default:
528 button.setDefault(True)
529 return button
532 def create_action_button(tooltip=None, icon=None):
533 button = QtGui.QPushButton()
534 button.setCursor(Qt.PointingHandCursor)
535 button.setFlat(True)
536 if tooltip is not None:
537 button.setToolTip(tooltip)
538 if icon is not None:
539 button.setIcon(icon)
540 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
541 return button
544 def ok_button(text, default=False, enabled=True):
545 return create_button(text=text, icon=icons.ok(),
546 default=default, enabled=enabled)
549 def close_button():
550 return create_button(text=N_('Close'), icon=icons.close())
553 def edit_button(enabled=True, default=False):
554 return create_button(text=N_('Edit'), icon=icons.edit(),
555 enabled=enabled, default=default)
558 def refresh_button(enabled=True, default=False):
559 return create_button(text=N_('Refresh'), icon=icons.sync(),
560 enabled=enabled, default=default)
563 def hide_button_menu_indicator(button):
564 cls = type(button)
565 name = cls.__name__
566 stylesheet = """
567 %(name)s::menu-indicator {
568 image: none;
571 if name == 'QPushButton':
572 stylesheet += """
573 %(name)s {
574 border-style: none;
577 button.setStyleSheet(stylesheet % {'name': name})
580 def checkbox(text='', tooltip='', checked=None):
581 cb = QtGui.QCheckBox()
582 if text:
583 cb.setText(text)
584 if tooltip:
585 cb.setToolTip(tooltip)
586 if checked is not None:
587 cb.setChecked(checked)
589 url = icons.check_name()
590 style = """
591 QCheckBox::indicator {
592 width: %(size)dpx;
593 height: %(size)dpx;
595 QCheckBox::indicator::unchecked {
596 border: %(border)dpx solid #999;
597 background: #fff;
599 QCheckBox::indicator::checked {
600 image: url(%(url)s);
601 border: %(border)dpx solid black;
602 background: #fff;
604 """ % dict(size=defs.checkbox, border=defs.border, url=url)
605 cb.setStyleSheet(style)
607 return cb
610 def radio(text='', tooltip='', checked=None):
611 rb = QtGui.QRadioButton()
612 if text:
613 rb.setText(text)
614 if tooltip:
615 rb.setToolTip(tooltip)
616 if checked is not None:
617 rb.setChecked(checked)
619 size = defs.checkbox
620 radius = size / 2
621 border = defs.radio_border
622 url = icons.dot_name()
623 style = """
624 QRadioButton::indicator {
625 width: %(size)dpx;
626 height: %(size)dpx;
628 QRadioButton::indicator::unchecked {
629 background: #fff;
630 border: %(border)dpx solid #999;
631 border-radius: %(radius)dpx;
633 QRadioButton::indicator::checked {
634 image: url(%(url)s);
635 background: #fff;
636 border: %(border)dpx solid black;
637 border-radius: %(radius)dpx;
639 """ % dict(size=size, radius=radius, border=border, url=url)
640 rb.setStyleSheet(style)
642 return rb
645 class DockTitleBarWidget(QtGui.QWidget):
647 def __init__(self, parent, title, stretch=True):
648 QtGui.QWidget.__init__(self, parent)
649 self.label = label = QtGui.QLabel()
650 font = label.font()
651 font.setBold(True)
652 label.setFont(font)
653 label.setText(title)
654 label.setCursor(Qt.OpenHandCursor)
656 self.close_button = create_action_button(
657 tooltip=N_('Close'), icon=icons.close())
659 self.toggle_button = create_action_button(
660 tooltip=N_('Detach'), icon=icons.external())
662 self.corner_layout = hbox(defs.no_margin, defs.spacing)
664 if stretch:
665 separator = STRETCH
666 else:
667 separator = SKIPPED
669 self.main_layout = hbox(defs.small_margin, defs.spacing,
670 label, separator, self.corner_layout,
671 self.toggle_button, self.close_button)
672 self.setLayout(self.main_layout)
674 connect_button(self.toggle_button, self.toggle_floating)
675 connect_button(self.close_button, self.toggle_visibility)
677 def toggle_floating(self):
678 self.parent().setFloating(not self.parent().isFloating())
679 self.update_tooltips()
681 def toggle_visibility(self):
682 self.parent().toggleViewAction().trigger()
684 def set_title(self, title):
685 self.label.setText(title)
687 def add_corner_widget(self, widget):
688 self.corner_layout.addWidget(widget)
690 def update_tooltips(self):
691 if self.parent().isFloating():
692 tooltip = N_('Attach')
693 else:
694 tooltip = N_('Detach')
695 self.toggle_button.setToolTip(tooltip)
698 def create_dock(title, parent, stretch=True):
699 """Create a dock widget and set it up accordingly."""
700 dock = QtGui.QDockWidget(parent)
701 dock.setWindowTitle(title)
702 dock.setObjectName(title)
703 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
704 dock.setTitleBarWidget(titlebar)
705 if hasattr(parent, 'dockwidgets'):
706 parent.dockwidgets.append(dock)
707 return dock
710 def create_menu(title, parent):
711 """Create a menu and set its title."""
712 qmenu = QtGui.QMenu(title, parent)
713 return qmenu
716 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
717 button = QtGui.QToolButton()
718 button.setAutoRaise(True)
719 button.setAutoFillBackground(True)
720 button.setCursor(Qt.PointingHandCursor)
721 if icon is not None:
722 button.setIcon(icon)
723 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
724 if text is not None:
725 button.setText(text)
726 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
727 if tooltip is not None:
728 button.setToolTip(tooltip)
729 if layout is not None:
730 layout.addWidget(button)
731 return button
734 def mimedata_from_paths(paths):
735 """Return mimedata with a list of absolute path URLs"""
737 abspaths = [core.abspath(path) for path in paths]
738 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
740 mimedata = QtCore.QMimeData()
741 mimedata.setUrls(urls)
743 # The text/x-moz-list format is always included by Qt, and doing
744 # mimedata.removeFormat('text/x-moz-url') has no effect.
745 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
747 # gnome-terminal expects utf-16 encoded text, but other terminals,
748 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
749 # to override the default.
750 paths_text = core.list2cmdline(abspaths)
751 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
752 moz_text = core.encode(paths_text, encoding=encoding)
753 mimedata.setData('text/x-moz-url', moz_text)
755 return mimedata
758 def path_mimetypes():
759 return ['text/uri-list', 'text/x-moz-url']
762 class BlockSignals(object):
763 """Context manager for blocking a signals on a widget"""
765 def __init__(self, *widgets):
766 self.widgets = widgets
767 self.values = {}
769 def __enter__(self):
770 for w in self.widgets:
771 self.values[w] = w.blockSignals(True)
772 return self
774 def __exit__(self, exc_type, exc_val, exc_tb):
775 for w in self.widgets:
776 w.blockSignals(self.values[w])
779 class Task(QtCore.QRunnable):
780 """Disable auto-deletion to avoid gc issues
782 Python's garbage collector will try to double-free the task
783 once it's finished, so disable Qt's auto-deletion as a workaround.
787 FINISHED = SIGNAL('TASK_FINISHED')
788 RESULT = SIGNAL('TASK_RESULT')
790 def __init__(self, parent, *args, **kwargs):
791 QtCore.QRunnable.__init__(self)
793 self.channel = QtCore.QObject(parent)
794 self.result = None
795 self.setAutoDelete(False)
797 def run(self):
798 self.result = self.task()
799 self.channel.emit(self.RESULT, self.result)
800 self.done()
802 def task(self):
803 pass
805 def done(self):
806 self.channel.emit(self.FINISHED, self)
808 def connect(self, handler):
809 self.channel.connect(self.channel, self.RESULT,
810 handler, Qt.QueuedConnection)
813 class SimpleTask(Task):
814 """Run a simple callable as a task"""
816 def __init__(self, parent, fn, *args, **kwargs):
817 Task.__init__(self, parent)
819 self.fn = fn
820 self.args = args
821 self.kwargs = kwargs
823 def task(self):
824 return self.fn(*self.args, **self.kwargs)
827 class RunTask(QtCore.QObject):
828 """Runs QRunnable instances and transfers control when they finish"""
830 def __init__(self, parent=None):
831 QtCore.QObject.__init__(self, parent)
832 self.tasks = []
833 self.task_details = {}
834 self.threadpool = QtCore.QThreadPool.globalInstance()
836 def start(self, task, progress=None, finish=None):
837 """Start the task and register a callback"""
838 if progress is not None:
839 progress.show()
840 # prevents garbage collection bugs in certain PyQt4 versions
841 self.tasks.append(task)
842 task_id = id(task)
843 self.task_details[task_id] = (progress, finish)
845 self.connect(task.channel, Task.FINISHED, self.finish,
846 Qt.QueuedConnection)
847 self.threadpool.start(task)
849 def finish(self, task, *args, **kwargs):
850 task_id = id(task)
851 try:
852 self.tasks.remove(task)
853 except:
854 pass
855 try:
856 progress, finish = self.task_details[task_id]
857 del self.task_details[task_id]
858 except KeyError:
859 finish = progress = None
861 if progress is not None:
862 progress.hide()
864 if finish is not None:
865 finish(task, *args, **kwargs)
868 # Syntax highlighting
870 def rgba(r, g, b, a=255):
871 c = QtGui.QColor()
872 c.setRgb(r, g, b)
873 c.setAlpha(a)
874 return c
877 def RGB(args):
878 return rgba(*args)
881 def make_format(fg=None, bg=None, bold=False):
882 fmt = QtGui.QTextCharFormat()
883 if fg:
884 fmt.setForeground(fg)
885 if bg:
886 fmt.setBackground(bg)
887 if bold:
888 fmt.setFontWeight(QtGui.QFont.Bold)
889 return fmt
892 def install():
893 Interaction.critical = staticmethod(critical)
894 Interaction.confirm = staticmethod(confirm)
895 Interaction.question = staticmethod(question)
896 Interaction.information = staticmethod(information)