1 # Copyright (c) 2008-2016 David Aguilar
2 """Miscellaneous Qt utility functions."""
3 from __future__
import division
, absolute_import
, unicode_literals
5 from qtpy
import compat
7 from qtpy
import QtCore
8 from qtpy
import QtWidgets
9 from qtpy
.QtCore
import Qt
10 from qtpy
.QtCore
import Signal
18 from .interaction
import Interaction
19 from .compat
import int_types
20 from .compat
import ustr
21 from .models
import prefs
22 from .widgets
import defs
29 def disconnect(signal
):
30 """Disconnect signal from all slots"""
33 except TypeError: # allow unconnected slots
37 def connect_action(action
, fn
):
38 """Connect an action to a function"""
39 action
.triggered
[bool].connect(lambda x
: fn())
42 def connect_action_bool(action
, fn
):
43 """Connect a triggered(bool) action to a function"""
44 action
.triggered
[bool].connect(fn
)
47 def connect_button(button
, fn
):
48 """Connect a button to a function"""
49 button
.pressed
.connect(fn
)
52 def button_action(button
, action
):
53 """Make a button trigger an action"""
54 connect_button(button
, action
.trigger
)
57 def connect_toggle(toggle
, fn
):
58 """Connect a toggle button to a function"""
59 toggle
.toggled
.connect(fn
)
63 """Return the active window for the current application"""
64 return QtWidgets
.QApplication
.activeWindow()
67 def hbox(margin
, spacing
, *items
):
68 """Create an HBoxLayout with the specified sizes and items"""
69 return box(QtWidgets
.QHBoxLayout
, margin
, spacing
, *items
)
72 def vbox(margin
, spacing
, *items
):
73 """Create a VBoxLayout with the specified sizes and items"""
74 return box(QtWidgets
.QVBoxLayout
, margin
, spacing
, *items
)
77 def buttongroup(*items
):
78 """Create a QButtonGroup for the specified items"""
79 group
= QtWidgets
.QButtonGroup()
85 def set_margin(layout
, margin
):
86 """Set the content margins for a layout"""
87 layout
.setContentsMargins(margin
, margin
, margin
, margin
)
90 def box(cls
, margin
, spacing
, *items
):
91 """Create a QBoxLayout with the specified sizes and items"""
95 layout
.setSpacing(spacing
)
96 set_margin(layout
, margin
)
99 if isinstance(i
, QtWidgets
.QWidget
):
101 elif isinstance(i
, (QtWidgets
.QHBoxLayout
, QtWidgets
.QVBoxLayout
,
102 QtWidgets
.QFormLayout
, QtWidgets
.QLayout
)):
108 elif isinstance(i
, int_types
):
114 def form(margin
, spacing
, *widgets
):
115 """Create a QFormLayout with the specified sizes and items"""
116 layout
= QtWidgets
.QFormLayout()
117 layout
.setSpacing(spacing
)
118 layout
.setFieldGrowthPolicy(QtWidgets
.QFormLayout
.ExpandingFieldsGrow
)
119 set_margin(layout
, margin
)
121 for idx
, (name
, widget
) in enumerate(widgets
):
122 if isinstance(name
, (str, ustr
)):
123 layout
.addRow(name
, widget
)
125 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.LabelRole
, name
)
126 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.FieldRole
, widget
)
131 def grid(margin
, spacing
, *widgets
):
132 """Create a QGridLayout with the specified sizes and items"""
133 layout
= QtWidgets
.QGridLayout()
134 layout
.setSpacing(spacing
)
135 set_margin(layout
, margin
)
139 if isinstance(item
, QtWidgets
.QWidget
):
140 layout
.addWidget(*row
)
141 elif isinstance(item
, QtWidgets
.QLayoutItem
):
147 def splitter(orientation
, *widgets
):
148 """Create a spliter over the specified widgets
150 :param orientation: Qt.Horizontal or Qt.Vertical
153 layout
= QtWidgets
.QSplitter()
154 layout
.setOrientation(orientation
)
155 layout
.setHandleWidth(defs
.handle_width
)
156 layout
.setChildrenCollapsible(True)
158 for idx
, widget
in enumerate(widgets
):
159 layout
.addWidget(widget
)
160 layout
.setStretchFactor(idx
, 1)
162 # Workaround for Qt not setting the WA_Hover property for QSplitter
163 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
164 layout
.handle(1).setAttribute(Qt
.WA_Hover
)
169 def label(text
=None, align
=None, fmt
=None, selectable
=True):
170 """Create a QLabel with the specified properties"""
171 widget
= QtWidgets
.QLabel()
172 if align
is not None:
173 widget
.setAlignment(align
)
175 widget
.setTextFormat(fmt
)
177 widget
.setTextInteractionFlags(Qt
.TextBrowserInteraction
)
178 widget
.setOpenExternalLinks(True)
184 def textbrowser(text
=None):
185 """Create a QTextBrowser for the specified text"""
186 widget
= QtWidgets
.QTextBrowser()
187 widget
.setOpenExternalLinks(True)
193 def add_completer(widget
, items
):
194 """Add simple completion to a widget"""
195 completer
= QtWidgets
.QCompleter(items
, widget
)
196 completer
.setCaseSensitivity(Qt
.CaseInsensitive
)
197 completer
.setCompletionMode(QtWidgets
.QCompleter
.InlineCompletion
)
198 widget
.setCompleter(completer
)
201 def prompt(msg
, title
=None, text
=''):
202 """Presents the user with an input widget and returns the input."""
205 result
= QtWidgets
.QInputDialog
.getText(
206 active_window(), msg
, title
,
207 QtWidgets
.QLineEdit
.Normal
, text
)
208 return (result
[0], result
[1])
211 def prompt_n(msg
, inputs
):
212 """Presents the user with N input widgets and returns the results"""
213 dialog
= QtWidgets
.QDialog(active_window())
214 dialog
.setWindowModality(Qt
.WindowModal
)
215 dialog
.setWindowTitle(msg
)
219 if len(k
+ v
) > len(long_value
):
222 metrics
= QtGui
.QFontMetrics(dialog
.font())
223 min_width
= metrics
.width(long_value
) + 100
226 dialog
.setMinimumWidth(min_width
)
228 ok_b
= ok_button(msg
, enabled
=False)
229 close_b
= close_button()
234 return [pair
[1].text().strip() for pair
in form_widgets
]
236 for name
, value
in inputs
:
237 lineedit
= QtWidgets
.QLineEdit()
238 # Enable the OK button only when all fields have been populated
239 lineedit
.textChanged
.connect(
240 lambda x
: ok_b
.setEnabled(all(get_values())))
242 lineedit
.setText(value
)
243 form_widgets
.append((name
, lineedit
))
246 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
247 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
,
248 STRETCH
, close_b
, ok_b
)
249 main_layout
= vbox(defs
.margin
, defs
.button_spacing
,
250 form_layout
, button_layout
)
251 dialog
.setLayout(main_layout
)
254 connect_button(ok_b
, dialog
.accept
)
255 connect_button(close_b
, dialog
.reject
)
257 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
259 ok
= accepted
and all(text
)
263 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
265 TYPE
= QtGui
.QStandardItem
.UserType
+ 101
267 def __init__(self
, path
, icon
, deleted
):
268 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
270 self
.deleted
= deleted
271 self
.setIcon(0, icons
.from_name(icon
))
272 self
.setText(0, path
)
278 def paths_from_indexes(model
, indexes
,
279 item_type
=TreeWidgetItem
.TYPE
,
281 """Return paths from a list of QStandardItemModel indexes"""
282 items
= [model
.itemFromIndex(i
) for i
in indexes
]
283 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
290 def paths_from_items(items
,
291 item_type
=TreeWidgetItem
.TYPE
,
293 """Return a list of paths from a list of items"""
294 if item_filter
is None:
295 item_filter
= _true_filter
296 return [i
.path
for i
in items
297 if i
.type() == item_type
and item_filter(i
)]
300 def confirm(title
, text
, informative_text
, ok_text
,
301 icon
=None, default
=True,
302 cancel_text
=None, cancel_icon
=None):
303 """Confirm that an action should take place"""
304 msgbox
= QtWidgets
.QMessageBox(active_window())
305 msgbox
.setWindowModality(Qt
.WindowModal
)
306 msgbox
.setWindowTitle(title
)
308 msgbox
.setInformativeText(informative_text
)
310 icon
= icons
.mkicon(icon
, icons
.ok
)
311 ok
= msgbox
.addButton(ok_text
, QtWidgets
.QMessageBox
.ActionRole
)
314 cancel
= msgbox
.addButton(QtWidgets
.QMessageBox
.Cancel
)
315 cancel_icon
= icons
.mkicon(cancel_icon
, icons
.close
)
316 cancel
.setIcon(cancel_icon
)
318 cancel
.setText(cancel_text
)
321 msgbox
.setDefaultButton(ok
)
323 msgbox
.setDefaultButton(cancel
)
325 return msgbox
.clickedButton() == ok
328 class ResizeableMessageBox(QtWidgets
.QMessageBox
):
330 def __init__(self
, parent
):
331 QtWidgets
.QMessageBox
.__init
__(self
, parent
)
332 self
.setMouseTracking(True)
333 self
.setSizeGripEnabled(True)
335 def event(self
, event
):
336 res
= QtWidgets
.QMessageBox
.event(self
, event
)
337 event_type
= event
.type()
338 if (event_type
== QtCore
.QEvent
.MouseMove
or
339 event_type
== QtCore
.QEvent
.MouseButtonPress
):
340 maxi
= QtCore
.QSize(defs
.max_size
, defs
.max_size
)
341 self
.setMaximumSize(maxi
)
342 text
= self
.findChild(QtWidgets
.QTextEdit
)
344 expand
= QtWidgets
.QSizePolicy
.Expanding
345 text
.setSizePolicy(QtWidgets
.QSizePolicy(expand
, expand
))
346 text
.setMaximumSize(maxi
)
350 def critical(title
, message
=None, details
=None):
351 """Show a warning with the provided title and message."""
354 mbox
= ResizeableMessageBox(active_window())
355 mbox
.setWindowTitle(title
)
356 mbox
.setTextFormat(Qt
.PlainText
)
357 mbox
.setText(message
)
358 mbox
.setIcon(QtWidgets
.QMessageBox
.Critical
)
359 mbox
.setStandardButtons(QtWidgets
.QMessageBox
.Close
)
360 mbox
.setDefaultButton(QtWidgets
.QMessageBox
.Close
)
362 mbox
.setDetailedText(details
)
366 def information(title
, message
=None, details
=None, informative_text
=None):
367 """Show information with the provided title and message."""
370 mbox
= QtWidgets
.QMessageBox(active_window())
371 mbox
.setStandardButtons(QtWidgets
.QMessageBox
.Close
)
372 mbox
.setDefaultButton(QtWidgets
.QMessageBox
.Close
)
373 mbox
.setWindowTitle(title
)
374 mbox
.setWindowModality(Qt
.WindowModal
)
375 mbox
.setTextFormat(Qt
.PlainText
)
376 mbox
.setText(message
)
378 mbox
.setInformativeText(informative_text
)
380 mbox
.setDetailedText(details
)
381 # Render into a 1-inch wide pixmap
382 pixmap
= icons
.cola().pixmap(defs
.large_icon
)
383 mbox
.setIconPixmap(pixmap
)
387 def question(title
, msg
, default
=True):
388 """Launches a QMessageBox question with the provided title and message.
389 Passing "default=False" will make "No" the default choice."""
390 yes
= QtWidgets
.QMessageBox
.Yes
391 no
= QtWidgets
.QMessageBox
.No
398 parent
= active_window()
399 MessageBox
= QtWidgets
.QMessageBox
400 result
= MessageBox
.question(parent
, title
, msg
, buttons
, default
)
401 return result
== QtWidgets
.QMessageBox
.Yes
404 def tree_selection(tree_item
, items
):
405 """Returns an array of model items that correspond to the selected
406 QTreeWidgetItem children"""
408 count
= min(tree_item
.childCount(), len(items
))
409 for idx
in range(count
):
410 if tree_item
.child(idx
).isSelected():
411 selected
.append(items
[idx
])
416 def tree_selection_items(tree_item
):
417 """Returns selected widget items"""
419 for idx
in range(tree_item
.childCount()):
420 child
= tree_item
.child(idx
)
421 if child
.isSelected():
422 selected
.append(child
)
427 def selected_item(list_widget
, items
):
428 """Returns the model item that corresponds to the selected QListWidget
430 widget_items
= list_widget
.selectedItems()
433 widget_item
= widget_items
[0]
434 row
= list_widget
.row(widget_item
)
441 def selected_items(list_widget
, items
):
442 """Returns an array of model items that correspond to the selected
444 item_count
= len(items
)
446 for widget_item
in list_widget
.selectedItems():
447 row
= list_widget
.row(widget_item
)
449 selected
.append(items
[row
])
453 def open_file(title
, directory
=None):
454 """Creates an Open File dialog and returns a filename."""
455 result
= compat
.getopenfilename(parent
=active_window(),
461 def open_files(title
, directory
=None, filters
=''):
462 """Creates an Open File dialog and returns a list of filenames."""
463 result
= compat
.getopenfilenames(parent
=active_window(),
470 def opendir_dialog(caption
, path
):
471 """Prompts for a directory path"""
473 options
= (QtWidgets
.QFileDialog
.ShowDirsOnly |
474 QtWidgets
.QFileDialog
.DontResolveSymlinks
)
475 return compat
.getexistingdirectory(parent
=active_window(),
481 def save_as(filename
, title
='Save As...'):
482 """Creates a Save File dialog and returns a filename."""
483 result
= compat
.getsavefilename(parent
=active_window(),
489 def copy_path(filename
, absolute
=True):
490 """Copy a filename to the clipboard"""
494 filename
= core
.abspath(filename
)
495 set_clipboard(filename
)
498 def set_clipboard(text
):
499 """Sets the copy/paste buffer to text."""
502 clipboard
= QtWidgets
.QApplication
.clipboard()
503 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
504 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
508 def persist_clipboard():
509 """Persist the clipboard
511 X11 stores only a reference to the clipboard data.
512 Send a clipboard event to force a copy of the clipboard to occur.
513 This ensures that the clipboard is present after git-cola exits.
514 Otherwise, the reference is destroyed on exit.
516 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
519 clipboard
= QtWidgets
.QApplication
.clipboard()
520 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
521 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
524 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
526 action
= _add_action(widget
, text
, tip
, fn
, connect_action_bool
, *shortcuts
)
527 action
.setCheckable(True)
528 action
.setChecked(checked
)
532 def add_action(widget
, text
, fn
, *shortcuts
):
534 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
537 def add_action_with_status_tip(widget
, text
, tip
, fn
, *shortcuts
):
538 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
541 def _add_action(widget
, text
, tip
, fn
, connect
, *shortcuts
):
542 action
= QtWidgets
.QAction(text
, widget
)
543 if hasattr(action
, 'setIconVisibleInMenu'):
544 action
.setIconVisibleInMenu(True)
546 action
.setStatusTip(tip
)
549 action
.setShortcuts(shortcuts
)
550 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
551 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
552 widget
.addAction(action
)
556 def set_selected_item(widget
, idx
):
557 """Sets a the currently selected item to the item at index idx."""
558 if type(widget
) is QtWidgets
.QTreeWidget
:
559 item
= widget
.topLevelItem(idx
)
561 item
.setSelected(True)
562 widget
.setCurrentItem(item
)
565 def add_items(widget
, items
):
566 """Adds items to a widget."""
573 def set_items(widget
, items
):
574 """Clear the existing widget contents and set the new items."""
576 add_items(widget
, items
)
579 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
580 """Given a filename, return a TreeWidgetItem for a status widget
582 "staged", "deleted, and "untracked" control which icon is used.
585 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
586 return TreeWidgetItem(filename
, icons
.name_from_basename(icon_name
),
590 def add_close_action(widget
):
591 """Adds close action and shortcuts to a widget."""
592 return add_action(widget
, N_('Close...'),
593 widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
597 """Return the current application"""
598 return QtWidgets
.QApplication
.instance()
602 """Return the desktop"""
603 return app().desktop()
606 def center_on_screen(widget
):
607 """Move widget to the center of the default screen"""
609 rect
= desk
.screenGeometry(QtGui
.QCursor().pos())
610 cy
= rect
.height()//2
612 widget
.move(cx
- widget
.width()//2, cy
- widget
.height()//2)
615 def default_size(parent
, width
, height
):
616 """Return the parent's size, or the provided defaults"""
617 if parent
is not None:
618 width
= parent
.width()
619 height
= parent
.height()
620 return (width
, height
)
623 def default_monospace_font():
626 if utils
.is_darwin():
628 font
.setFamily(family
)
633 font_str
= gitcfg
.current().get(prefs
.FONTDIFF
)
635 font_str
= default_monospace_font().toString()
640 return font(diff_font_str())
645 font
.fromString(string
)
649 def create_button(text
='', layout
=None, tooltip
=None, icon
=None,
650 enabled
=True, default
=False):
651 """Create a button, set its title, and add it to the parent."""
652 button
= QtWidgets
.QPushButton()
653 button
.setCursor(Qt
.PointingHandCursor
)
658 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
659 if tooltip
is not None:
660 button
.setToolTip(tooltip
)
661 if layout
is not None:
662 layout
.addWidget(button
)
664 button
.setEnabled(False)
666 button
.setDefault(True)
670 def create_action_button(tooltip
=None, icon
=None):
671 button
= QtWidgets
.QPushButton()
672 button
.setCursor(Qt
.PointingHandCursor
)
674 if tooltip
is not None:
675 button
.setToolTip(tooltip
)
678 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
682 def ok_button(text
, default
=True, enabled
=True, icon
=None):
685 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
689 return create_button(text
=N_('Close'), icon
=icons
.close())
692 def edit_button(enabled
=True, default
=False):
693 return create_button(text
=N_('Edit'), icon
=icons
.edit(),
694 enabled
=enabled
, default
=default
)
697 def refresh_button(enabled
=True, default
=False):
698 return create_button(text
=N_('Refresh'), icon
=icons
.sync(),
699 enabled
=enabled
, default
=default
)
702 def hide_button_menu_indicator(button
):
703 """Hide the menu indicator icon on buttons"""
705 name
= button
.__class
__.__name
__
707 %(name)s::menu-indicator {
711 if name
== 'QPushButton':
717 button
.setStyleSheet(stylesheet
% dict(name
=name
))
720 def checkbox(text
='', tooltip
='', checked
=None):
721 """Create a checkbox"""
722 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
725 def radio(text
='', tooltip
='', checked
=None):
726 """Create a radio button"""
727 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
730 def _checkbox(cls
, text
, tooltip
, checked
):
731 """Create a widget and apply properties"""
736 widget
.setToolTip(tooltip
)
737 if checked
is not None:
738 widget
.setChecked(checked
)
742 class DockTitleBarWidget(QtWidgets
.QWidget
):
744 def __init__(self
, parent
, title
, stretch
=True):
745 QtWidgets
.QWidget
.__init
__(self
, parent
)
746 self
.setAutoFillBackground(True)
747 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
751 qlabel
.setCursor(Qt
.OpenHandCursor
)
753 self
.close_button
= create_action_button(
754 tooltip
=N_('Close'), icon
=icons
.close())
756 self
.toggle_button
= create_action_button(
757 tooltip
=N_('Detach'), icon
=icons
.external())
759 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
766 self
.main_layout
= hbox(defs
.small_margin
, defs
.spacing
,
767 qlabel
, separator
, self
.corner_layout
,
768 self
.toggle_button
, self
.close_button
)
769 self
.setLayout(self
.main_layout
)
771 connect_button(self
.toggle_button
, self
.toggle_floating
)
772 connect_button(self
.close_button
, self
.toggle_visibility
)
774 def toggle_floating(self
):
775 self
.parent().setFloating(not self
.parent().isFloating())
776 self
.update_tooltips()
778 def toggle_visibility(self
):
779 self
.parent().toggleViewAction().trigger()
781 def set_title(self
, title
):
782 self
.label
.setText(title
)
784 def add_corner_widget(self
, widget
):
785 self
.corner_layout
.addWidget(widget
)
787 def update_tooltips(self
):
788 if self
.parent().isFloating():
789 tooltip
= N_('Attach')
791 tooltip
= N_('Detach')
792 self
.toggle_button
.setToolTip(tooltip
)
795 def create_dock(title
, parent
, stretch
=True):
796 """Create a dock widget and set it up accordingly."""
797 dock
= QtWidgets
.QDockWidget(parent
)
798 dock
.setWindowTitle(title
)
799 dock
.setObjectName(title
)
800 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
801 dock
.setTitleBarWidget(titlebar
)
802 dock
.setAutoFillBackground(True)
803 if hasattr(parent
, 'dockwidgets'):
804 parent
.dockwidgets
.append(dock
)
808 def create_menu(title
, parent
):
809 """Create a menu and set its title."""
810 qmenu
= QtWidgets
.QMenu(title
, parent
)
814 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
815 button
= QtWidgets
.QToolButton()
816 button
.setAutoRaise(True)
817 button
.setAutoFillBackground(True)
818 button
.setCursor(Qt
.PointingHandCursor
)
821 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
824 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
825 if tooltip
is not None:
826 button
.setToolTip(tooltip
)
827 if layout
is not None:
828 layout
.addWidget(button
)
832 def mimedata_from_paths(paths
):
833 """Return mimedata with a list of absolute path URLs"""
835 abspaths
= [core
.abspath(path
) for path
in paths
]
836 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
838 mimedata
= QtCore
.QMimeData()
839 mimedata
.setUrls(urls
)
841 # The text/x-moz-list format is always included by Qt, and doing
842 # mimedata.removeFormat('text/x-moz-url') has no effect.
843 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
845 # gnome-terminal expects utf-16 encoded text, but other terminals,
846 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
847 # to override the default.
848 paths_text
= core
.list2cmdline(abspaths
)
849 encoding
= gitcfg
.current().get('cola.dragencoding', 'utf-16')
850 moz_text
= core
.encode(paths_text
, encoding
=encoding
)
851 mimedata
.setData('text/x-moz-url', moz_text
)
856 def path_mimetypes():
857 return ['text/uri-list', 'text/x-moz-url']
860 class BlockSignals(object):
861 """Context manager for blocking a signals on a widget"""
863 def __init__(self
, *widgets
):
864 self
.widgets
= widgets
868 for w
in self
.widgets
:
869 self
.values
[w
] = w
.blockSignals(True)
872 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
873 for w
in self
.widgets
:
874 w
.blockSignals(self
.values
[w
])
877 class Channel(QtCore
.QObject
):
878 finished
= Signal(object)
879 result
= Signal(object)
882 class Task(QtCore
.QRunnable
):
883 """Disable auto-deletion to avoid gc issues
885 Python's garbage collector will try to double-free the task
886 once it's finished, so disable Qt's auto-deletion as a workaround.
890 def __init__(self
, parent
):
891 QtCore
.QRunnable
.__init
__(self
)
893 self
.channel
= Channel()
895 self
.setAutoDelete(False)
898 self
.result
= self
.task()
899 self
.channel
.result
.emit(self
.result
)
906 self
.channel
.finished
.emit(self
)
908 def connect(self
, handler
):
909 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
912 class SimpleTask(Task
):
913 """Run a simple callable as a task"""
915 def __init__(self
, parent
, fn
, *args
, **kwargs
):
916 Task
.__init
__(self
, parent
)
923 return self
.fn(*self
.args
, **self
.kwargs
)
926 class RunTask(QtCore
.QObject
):
927 """Runs QRunnable instances and transfers control when they finish"""
929 def __init__(self
, parent
=None):
930 QtCore
.QObject
.__init
__(self
, parent
)
932 self
.task_details
= {}
933 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
935 def start(self
, task
, progress
=None, finish
=None):
936 """Start the task and register a callback"""
937 if progress
is not None:
939 # prevents garbage collection bugs in certain PyQt4 versions
940 self
.tasks
.append(task
)
942 self
.task_details
[task_id
] = (progress
, finish
)
944 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
945 self
.threadpool
.start(task
)
947 def finish(self
, task
):
950 self
.tasks
.remove(task
)
954 progress
, finish
= self
.task_details
[task_id
]
955 del self
.task_details
[task_id
]
957 finish
= progress
= None
959 if progress
is not None:
962 if finish
is not None:
966 # Syntax highlighting
968 def rgba(r
, g
, b
, a
=255):
980 """Convert a QColor into an rgb(int, int, int) CSS string"""
981 return 'rgb(%d, %d, %d)' % (color
.red(), color
.green(), color
.blue())
984 def make_format(fg
=None, bg
=None, bold
=False):
985 fmt
= QtGui
.QTextCharFormat()
987 fmt
.setForeground(fg
)
989 fmt
.setBackground(bg
)
991 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
996 Interaction
.critical
= staticmethod(critical
)
997 Interaction
.confirm
= staticmethod(confirm
)
998 Interaction
.question
= staticmethod(question
)
999 Interaction
.information
= staticmethod(information
)