cmds: add SetDefaultRepo command
[git-cola.git] / cola / qtutils.py
blob29a35ecd6eee2d76f65b7836e33993268a848252
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 def buttongroup(*items):
64 group = QtGui.QButtonGroup()
65 for i in items:
66 group.addButton(i)
67 return group
70 STRETCH = object()
71 SKIPPED = object()
74 def box(cls, margin, spacing, *items):
75 stretch = STRETCH
76 skipped = SKIPPED
77 layout = cls()
78 layout.setMargin(margin)
79 layout.setSpacing(spacing)
81 if PY3:
82 int_types = (int,)
83 else:
84 int_types = (int, long)
86 for i in items:
87 if isinstance(i, QtGui.QWidget):
88 layout.addWidget(i)
89 elif isinstance(i, (QtGui.QHBoxLayout, QtGui.QVBoxLayout,
90 QtGui.QFormLayout, QtGui.QLayout)):
91 layout.addLayout(i)
92 elif i is stretch:
93 layout.addStretch()
94 elif i is skipped:
95 continue
96 elif isinstance(i, int_types):
97 layout.addSpacing(i)
99 return layout
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)
111 else:
112 layout.setWidget(idx, QtGui.QFormLayout.LabelRole, label)
113 layout.setWidget(idx, QtGui.QFormLayout.FieldRole, widget)
115 return layout
118 def grid(margin, spacing, *widgets):
119 layout = QtGui.QGridLayout()
120 layout.setMargin(defs.no_margin)
121 layout.setSpacing(defs.spacing)
123 for row in widgets:
124 item = row[0]
125 if isinstance(item, QtGui.QWidget):
126 layout.addWidget(*row)
127 elif isinstance(item, QtGui.QLayoutItem):
128 layout.addItem(*row)
130 return layout
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)
142 return layout
145 def prompt(msg, title=None, text=''):
146 """Presents the user with an input widget and returns the input."""
147 if title is None:
148 title = msg
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))
159 item.setText(text)
160 return item
163 class TreeWidgetItem(QtGui.QTreeWidgetItem):
165 TYPE = QtGui.QStandardItem.UserType + 101
167 def __init__(self, path, icon, deleted):
168 QtGui.QTreeWidgetItem.__init__(self)
169 self.path = path
170 self.deleted = deleted
171 self.setIcon(0, icons.from_name(icon))
172 self.setText(0, path)
174 def type(self):
175 return self.TYPE
178 def paths_from_indexes(model, indexes,
179 item_type=TreeWidgetItem.TYPE,
180 item_filter=None):
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,
188 item_filter=None):
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)
203 msgbox.setText(text)
204 msgbox.setInformativeText(informative_text)
206 icon = icons.mkicon(icon, icons.ok)
207 ok = msgbox.addButton(ok_text, QtGui.QMessageBox.ActionRole)
208 ok.setIcon(icon)
210 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
211 cancel_icon = icons.mkicon(cancel_icon, icons.close)
212 cancel.setIcon(cancel_icon)
213 if cancel_text:
214 cancel.setText(cancel_text)
216 if default:
217 msgbox.setDefaultButton(ok)
218 else:
219 msgbox.setDefaultButton(cancel)
220 msgbox.exec_()
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)
239 if text is not None:
240 expand = QtGui.QSizePolicy.Expanding
241 text.setSizePolicy(QtGui.QSizePolicy(expand, expand))
242 text.setMaximumSize(maxi)
243 return res
246 def critical(title, message=None, details=None):
247 """Show a warning with the provided title and message."""
248 if message is None:
249 message = title
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)
257 if details:
258 mbox.setDetailedText(details)
259 mbox.exec_()
262 def information(title, message=None, details=None, informative_text=None):
263 """Show information with the provided title and message."""
264 if message is None:
265 message = title
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)
273 if informative_text:
274 mbox.setInformativeText(informative_text)
275 if details:
276 mbox.setDetailedText(details)
277 # Render into a 1-inch wide pixmap
278 pixmap = icons.cola().pixmap(defs.large_icon)
279 mbox.setIconPixmap(pixmap)
280 mbox.exec_()
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
288 buttons = yes | no
289 if default:
290 default = yes
291 else:
292 default = 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"""
301 selected = []
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])
307 return selected
310 def tree_selection_items(tree_item):
311 """Returns selected widget items"""
312 selected = []
313 for idx in range(tree_item.childCount()):
314 child = tree_item.child(idx)
315 if child.isSelected():
316 selected.append(child)
318 return selected
321 def selected_item(list_widget, items):
322 """Returns the model item that corresponds to the selected QListWidget
323 row."""
324 widget_items = list_widget.selectedItems()
325 if not widget_items:
326 return None
327 widget_item = widget_items[0]
328 row = list_widget.row(widget_item)
329 if row < len(items):
330 return items[row]
331 else:
332 return None
335 def selected_items(list_widget, items):
336 """Returns an array of model items that correspond to the selected
337 QListWidget rows."""
338 item_count = len(items)
339 selected = []
340 for widget_item in list_widget.selectedItems():
341 row = list_widget.row(widget_item)
342 if row < item_count:
343 selected.append(items[row])
344 return selected
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(),
366 title, path, flags))
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"""
377 if filename is None:
378 return
379 if absolute:
380 filename = core.abspath(filename)
381 set_clipboard(filename)
384 def set_clipboard(text):
385 """Sets the copy/paste buffer to text."""
386 if not text:
387 return
388 clipboard = QtGui.QApplication.clipboard()
389 clipboard.setText(text, QtGui.QClipboard.Clipboard)
390 clipboard.setText(text, QtGui.QClipboard.Selection)
391 persist_clipboard()
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):
411 tip = text
412 action = _add_action(widget, text, tip, fn, connect_action_bool, *shortcuts)
413 action.setCheckable(True)
414 action.setChecked(checked)
415 return action
418 def add_action(widget, text, fn, *shortcuts):
419 tip = text
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)
429 if tip:
430 action.setStatusTip(tip)
431 connect(action, fn)
432 if shortcuts:
433 action.setShortcuts(shortcuts)
434 if hasattr(Qt, 'WidgetWithChildrenShortcut'):
435 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
436 widget.addAction(action)
437 return 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)
444 if item:
445 widget.setItemSelected(item, True)
446 widget.setCurrentItem(item)
449 def add_items(widget, items):
450 """Adds items to a widget."""
451 for item in items:
452 if item is None:
453 continue
454 widget.addItem(item)
457 def set_items(widget, items):
458 """Clear the existing widget contents and set the new items."""
459 widget.clear()
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
485 cx = rect.width()//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():
498 font = QtGui.QFont()
499 family = 'Monospace'
500 if utils.is_darwin():
501 family = 'Monaco'
502 font.setFamily(family)
503 return font
506 def diff_font_str():
507 font_str = gitcfg.current().get(prefs.FONTDIFF)
508 if font_str is None:
509 font = default_monospace_font()
510 font_str = ustr(font.toString())
511 return font_str
514 def diff_font():
515 return font(diff_font_str())
518 def font(string):
519 font = QtGui.QFont()
520 font.fromString(string)
521 return font
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)
529 if text:
530 button.setText(text)
531 if icon is not None:
532 button.setIcon(icon)
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)
538 if not enabled:
539 button.setEnabled(False)
540 if default:
541 button.setDefault(True)
542 return button
545 def create_action_button(tooltip=None, icon=None):
546 button = QtGui.QPushButton()
547 button.setCursor(Qt.PointingHandCursor)
548 button.setFlat(True)
549 if tooltip is not None:
550 button.setToolTip(tooltip)
551 if icon is not None:
552 button.setIcon(icon)
553 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
554 return button
557 def ok_button(text, default=False, enabled=True):
558 return create_button(text=text, icon=icons.ok(),
559 default=default, enabled=enabled)
562 def close_button():
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):
577 cls = type(button)
578 name = cls.__name__
579 stylesheet = """
580 %(name)s::menu-indicator {
581 image: none;
584 if name == 'QPushButton':
585 stylesheet += """
586 %(name)s {
587 border-style: none;
590 button.setStyleSheet(stylesheet % {'name': name})
593 def checkbox(text='', tooltip='', checked=None):
594 cb = QtGui.QCheckBox()
595 if text:
596 cb.setText(text)
597 if tooltip:
598 cb.setToolTip(tooltip)
599 if checked is not None:
600 cb.setChecked(checked)
602 url = icons.check_name()
603 style = """
604 QCheckBox::indicator {
605 width: %(size)dpx;
606 height: %(size)dpx;
608 QCheckBox::indicator::unchecked {
609 border: %(border)dpx solid #999;
610 background: #fff;
612 QCheckBox::indicator::checked {
613 image: url(%(url)s);
614 border: %(border)dpx solid black;
615 background: #fff;
617 """ % dict(size=defs.checkbox, border=defs.border, url=url)
618 cb.setStyleSheet(style)
620 return cb
623 def radio(text='', tooltip='', checked=None):
624 rb = QtGui.QRadioButton()
625 if text:
626 rb.setText(text)
627 if tooltip:
628 rb.setToolTip(tooltip)
629 if checked is not None:
630 rb.setChecked(checked)
632 size = defs.checkbox
633 radius = size / 2
634 border = defs.radio_border
635 url = icons.dot_name()
636 style = """
637 QRadioButton::indicator {
638 width: %(size)dpx;
639 height: %(size)dpx;
641 QRadioButton::indicator::unchecked {
642 background: #fff;
643 border: %(border)dpx solid #999;
644 border-radius: %(radius)dpx;
646 QRadioButton::indicator::checked {
647 image: url(%(url)s);
648 background: #fff;
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)
655 return rb
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()
663 font = label.font()
664 font.setBold(True)
665 label.setFont(font)
666 label.setText(title)
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)
677 if stretch:
678 separator = STRETCH
679 else:
680 separator = SKIPPED
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')
706 else:
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)
720 return dock
723 def create_menu(title, parent):
724 """Create a menu and set its title."""
725 qmenu = QtGui.QMenu(parent)
726 qmenu.setTitle(title)
727 return qmenu
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)
735 if icon is not None:
736 button.setIcon(icon)
737 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
738 if text is not None:
739 button.setText(text)
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)
745 return 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)
769 return mimedata
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
781 self.values = {}
783 def __enter__(self):
784 for w in self.widgets:
785 self.values[w] = w.blockSignals(True)
786 return self
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)
808 self.result = None
809 self.setAutoDelete(False)
811 def run(self):
812 self.result = self.task()
813 self.channel.emit(self.RESULT, self.result)
814 self.done()
816 def task(self):
817 pass
819 def done(self):
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)
833 self.fn = fn
834 self.args = args
835 self.kwargs = kwargs
837 def task(self):
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)
846 self.tasks = []
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:
853 progress.show()
854 # prevents garbage collection bugs in certain PyQt4 versions
855 self.tasks.append(task)
856 task_id = id(task)
857 self.task_details[task_id] = (progress, finish)
859 self.connect(task.channel, Task.FINISHED, self.finish,
860 Qt.QueuedConnection)
861 self.threadpool.start(task)
863 def finish(self, task, *args, **kwargs):
864 task_id = id(task)
865 try:
866 self.tasks.remove(task)
867 except:
868 pass
869 try:
870 progress, finish = self.task_details[task_id]
871 del self.task_details[task_id]
872 except KeyError:
873 finish = progress = None
875 if progress is not None:
876 progress.hide()
878 if finish is not None:
879 finish(task, *args, **kwargs)
882 # Syntax highlighting
884 def rgba(r, g, b, a=255):
885 c = QtGui.QColor()
886 c.setRgb(r, g, b)
887 c.setAlpha(a)
888 return c
891 def RGB(args):
892 return rgba(*args)
895 def make_format(fg=None, bg=None, bold=False):
896 fmt = QtGui.QTextCharFormat()
897 if fg:
898 fmt.setForeground(fg)
899 if bg:
900 fmt.setBackground(bg)
901 if bold:
902 fmt.setFontWeight(QtGui.QFont.Bold)
903 return fmt
906 def install():
907 Interaction.critical = staticmethod(critical)
908 Interaction.confirm = staticmethod(confirm)
909 Interaction.question = staticmethod(question)
910 Interaction.information = staticmethod(information)