dag: avoid "long" on python3
[git-cola.git] / cola / qtutils.py
blob4d03a0957672a99dbd81acf2c42a3352747372e7
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 widget.addItem(item)
448 def set_items(widget, items):
449 """Clear the existing widget contents and set the new items."""
450 widget.clear()
451 add_items(widget, items)
455 def create_treeitem(filename, staged=False, deleted=False, untracked=False):
456 """Given a filename, return a TreeWidgetItem for a status widget
458 "staged", "deleted, and "untracked" control which icon is used.
461 icon_name = icons.status(filename, deleted, staged, untracked)
462 return TreeWidgetItem(filename, resources.icon(icon_name), deleted=deleted)
465 def add_close_action(widget):
466 """Adds close action and shortcuts to a widget."""
467 return add_action(widget, N_('Close...'),
468 widget.close, hotkeys.CLOSE, hotkeys.QUIT)
471 def center_on_screen(widget):
472 """Move widget to the center of the default screen"""
473 desktop = QtGui.QApplication.instance().desktop()
474 rect = desktop.screenGeometry(QtGui.QCursor().pos())
475 cy = rect.height()//2
476 cx = rect.width()//2
477 widget.move(cx - widget.width()//2, cy - widget.height()//2)
480 def default_size(parent, width, height):
481 """Return the parent's size, or the provided defaults"""
482 if parent is not None:
483 width = parent.width()
484 height = parent.height()
485 return (width, height)
488 def default_monospace_font():
489 font = QtGui.QFont()
490 family = 'Monospace'
491 if utils.is_darwin():
492 family = 'Monaco'
493 font.setFamily(family)
494 return font
497 def diff_font_str():
498 font_str = gitcfg.current().get(prefs.FONTDIFF)
499 if font_str is None:
500 font = default_monospace_font()
501 font_str = ustr(font.toString())
502 return font_str
505 def diff_font():
506 return font(diff_font_str())
509 def font(string):
510 font = QtGui.QFont()
511 font.fromString(string)
512 return font
515 def create_button(text='', layout=None, tooltip=None, icon=None,
516 enabled=True, default=False):
517 """Create a button, set its title, and add it to the parent."""
518 button = QtGui.QPushButton()
519 button.setCursor(Qt.PointingHandCursor)
520 if text:
521 button.setText(text)
522 if icon is not None:
523 button.setIcon(icon)
524 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
525 if tooltip is not None:
526 button.setToolTip(tooltip)
527 if layout is not None:
528 layout.addWidget(button)
529 if not enabled:
530 button.setEnabled(False)
531 if default:
532 button.setDefault(True)
533 return button
536 def create_action_button(tooltip=None, icon=None):
537 button = QtGui.QPushButton()
538 button.setCursor(Qt.PointingHandCursor)
539 button.setFlat(True)
540 if tooltip is not None:
541 button.setToolTip(tooltip)
542 if icon is not None:
543 button.setIcon(icon)
544 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
545 return button
548 def ok_button(text, default=False, enabled=True):
549 return create_button(text=text, icon=icons.ok(),
550 default=default, enabled=enabled)
553 def close_button():
554 return create_button(text=N_('Close'), icon=icons.close())
557 def edit_button(enabled=True, default=False):
558 return create_button(text=N_('Edit'), icon=icons.edit(),
559 enabled=enabled, default=default)
562 def refresh_button(enabled=True, default=False):
563 return create_button(text=N_('Refresh'), icon=icons.sync(),
564 enabled=enabled, default=default)
567 def hide_button_menu_indicator(button):
568 cls = type(button)
569 name = cls.__name__
570 stylesheet = """
571 %(name)s::menu-indicator {
572 image: none;
575 if name == 'QPushButton':
576 stylesheet += """
577 %(name)s {
578 border-style: none;
581 button.setStyleSheet(stylesheet % {'name': name})
584 def checkbox(text='', tooltip='', checked=None):
585 cb = QtGui.QCheckBox()
586 if text:
587 cb.setText(text)
588 if tooltip:
589 cb.setToolTip(tooltip)
590 if checked is not None:
591 cb.setChecked(checked)
593 url = icons.check_name()
594 style = """
595 QCheckBox::indicator {
596 width: %(size)dpx;
597 height: %(size)dpx;
599 QCheckBox::indicator::unchecked {
600 border: %(border)dpx solid #999;
601 background: #fff;
603 QCheckBox::indicator::checked {
604 image: url(%(url)s);
605 border: %(border)dpx solid black;
606 background: #fff;
608 """ % dict(size=defs.checkbox, border=defs.border, url=url)
609 cb.setStyleSheet(style)
611 return cb
614 def radio(text='', tooltip='', checked=None):
615 rb = QtGui.QRadioButton()
616 if text:
617 rb.setText(text)
618 if tooltip:
619 rb.setToolTip(tooltip)
620 if checked is not None:
621 rb.setChecked(checked)
623 size = defs.checkbox
624 radius = size / 2
625 border = defs.radio_border
626 url = icons.dot_name()
627 style = """
628 QRadioButton::indicator {
629 width: %(size)dpx;
630 height: %(size)dpx;
632 QRadioButton::indicator::unchecked {
633 background: #fff;
634 border: %(border)dpx solid #999;
635 border-radius: %(radius)dpx;
637 QRadioButton::indicator::checked {
638 image: url(%(url)s);
639 background: #fff;
640 border: %(border)dpx solid black;
641 border-radius: %(radius)dpx;
643 """ % dict(size=size, radius=radius, border=border, url=url)
644 rb.setStyleSheet(style)
646 return rb
649 class DockTitleBarWidget(QtGui.QWidget):
651 def __init__(self, parent, title, stretch=True):
652 QtGui.QWidget.__init__(self, parent)
653 self.label = label = QtGui.QLabel()
654 font = label.font()
655 font.setBold(True)
656 label.setFont(font)
657 label.setText(title)
658 label.setCursor(Qt.OpenHandCursor)
660 self.close_button = create_action_button(
661 tooltip=N_('Close'), icon=icons.close())
663 self.toggle_button = create_action_button(
664 tooltip=N_('Detach'), icon=icons.external())
666 self.corner_layout = hbox(defs.no_margin, defs.spacing)
668 if stretch:
669 separator = STRETCH
670 else:
671 separator = SKIPPED
673 self.main_layout = hbox(defs.small_margin, defs.spacing,
674 label, separator, self.corner_layout,
675 self.toggle_button, self.close_button)
676 self.setLayout(self.main_layout)
678 connect_button(self.toggle_button, self.toggle_floating)
679 connect_button(self.close_button, self.toggle_visibility)
681 def toggle_floating(self):
682 self.parent().setFloating(not self.parent().isFloating())
683 self.update_tooltips()
685 def toggle_visibility(self):
686 self.parent().toggleViewAction().trigger()
688 def set_title(self, title):
689 self.label.setText(title)
691 def add_corner_widget(self, widget):
692 self.corner_layout.addWidget(widget)
694 def update_tooltips(self):
695 if self.parent().isFloating():
696 tooltip = N_('Attach')
697 else:
698 tooltip = N_('Detach')
699 self.toggle_button.setToolTip(tooltip)
702 def create_dock(title, parent, stretch=True):
703 """Create a dock widget and set it up accordingly."""
704 dock = QtGui.QDockWidget(parent)
705 dock.setWindowTitle(title)
706 dock.setObjectName(title)
707 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
708 dock.setTitleBarWidget(titlebar)
709 if hasattr(parent, 'dockwidgets'):
710 parent.dockwidgets.append(dock)
711 return dock
714 def create_menu(title, parent):
715 """Create a menu and set its title."""
716 qmenu = QtGui.QMenu(parent)
717 qmenu.setTitle(title)
718 return qmenu
721 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
722 button = QtGui.QToolButton()
723 button.setAutoRaise(True)
724 button.setAutoFillBackground(True)
725 button.setCursor(Qt.PointingHandCursor)
726 if icon is not None:
727 button.setIcon(icon)
728 button.setIconSize(QtCore.QSize(defs.small_icon, defs.small_icon))
729 if text is not None:
730 button.setText(text)
731 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
732 if tooltip is not None:
733 button.setToolTip(tooltip)
734 if layout is not None:
735 layout.addWidget(button)
736 return button
739 def mimedata_from_paths(paths):
740 """Return mimedata with a list of absolute path URLs"""
742 abspaths = [core.abspath(path) for path in paths]
743 urls = [QtCore.QUrl.fromLocalFile(path) for path in abspaths]
745 mimedata = QtCore.QMimeData()
746 mimedata.setUrls(urls)
748 # The text/x-moz-list format is always included by Qt, and doing
749 # mimedata.removeFormat('text/x-moz-url') has no effect.
750 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
752 # gnome-terminal expects utf-16 encoded text, but other terminals,
753 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
754 # to override the default.
755 paths_text = subprocess.list2cmdline(abspaths)
756 encoding = gitcfg.current().get('cola.dragencoding', 'utf-16')
757 moz_text = core.encode(paths_text, encoding=encoding)
758 mimedata.setData('text/x-moz-url', moz_text)
760 return mimedata
763 def path_mimetypes():
764 return ['text/uri-list', 'text/x-moz-url']
767 class BlockSignals(object):
768 """Context manager for blocking a signals on a widget"""
770 def __init__(self, *widgets):
771 self.widgets = widgets
772 self.values = {}
774 def __enter__(self):
775 for w in self.widgets:
776 self.values[w] = w.blockSignals(True)
777 return self
779 def __exit__(self, exc_type, exc_val, exc_tb):
780 for w in self.widgets:
781 w.blockSignals(self.values[w])
784 class Task(QtCore.QRunnable):
785 """Disable auto-deletion to avoid gc issues
787 Python's garbage collector will try to double-free the task
788 once it's finished, so disable Qt's auto-deletion as a workaround.
792 FINISHED = SIGNAL('TASK_FINISHED')
793 RESULT = SIGNAL('TASK_RESULT')
795 def __init__(self, parent, *args, **kwargs):
796 QtCore.QRunnable.__init__(self)
798 self.channel = QtCore.QObject(parent)
799 self.result = None
800 self.setAutoDelete(False)
802 def run(self):
803 self.result = self.task()
804 self.channel.emit(self.RESULT, self.result)
805 self.done()
807 def task(self):
808 pass
810 def done(self):
811 self.channel.emit(self.FINISHED, self)
813 def connect(self, handler):
814 self.channel.connect(self.channel, self.RESULT,
815 handler, Qt.QueuedConnection)
818 class SimpleTask(Task):
819 """Run a simple callable as a task"""
821 def __init__(self, parent, fn, *args, **kwargs):
822 Task.__init__(self, parent)
824 self.fn = fn
825 self.args = args
826 self.kwargs = kwargs
828 def task(self):
829 return self.fn(*self.args, **self.kwargs)
832 class RunTask(QtCore.QObject):
833 """Runs QRunnable instances and transfers control when they finish"""
835 def __init__(self, parent=None):
836 QtCore.QObject.__init__(self, parent)
837 self.tasks = []
838 self.task_details = {}
839 self.threadpool = QtCore.QThreadPool.globalInstance()
841 def start(self, task, progress=None, finish=None):
842 """Start the task and register a callback"""
843 if progress is not None:
844 progress.show()
845 # prevents garbage collection bugs in certain PyQt4 versions
846 self.tasks.append(task)
847 task_id = id(task)
848 self.task_details[task_id] = (progress, finish)
850 self.connect(task.channel, Task.FINISHED, self.finish,
851 Qt.QueuedConnection)
852 self.threadpool.start(task)
854 def finish(self, task, *args, **kwargs):
855 task_id = id(task)
856 try:
857 self.tasks.remove(task)
858 except:
859 pass
860 try:
861 progress, finish = self.task_details[task_id]
862 del self.task_details[task_id]
863 except KeyError:
864 finish = progress = None
866 if progress is not None:
867 progress.hide()
869 if finish is not None:
870 finish(task, *args, **kwargs)
873 # Syntax highlighting
875 def rgba(r, g, b, a=255):
876 c = QtGui.QColor()
877 c.setRgb(r, g, b)
878 c.setAlpha(a)
879 return c
882 def RGB(args):
883 return rgba(*args)
886 def make_format(fg=None, bg=None, bold=False):
887 fmt = QtGui.QTextCharFormat()
888 if fg:
889 fmt.setForeground(fg)
890 if bg:
891 fmt.setBackground(bg)
892 if bold:
893 fmt.setFontWeight(QtGui.QFont.Bold)
894 return fmt
897 def install():
898 Interaction.critical = staticmethod(critical)
899 Interaction.confirm = staticmethod(confirm)
900 Interaction.question = staticmethod(question)
901 Interaction.information = staticmethod(information)