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 connect_released(button
, fn
):
53 """Connect a button to a function"""
54 button
.released
.connect(fn
)
57 def button_action(button
, action
):
58 """Make a button trigger an action"""
59 connect_button(button
, action
.trigger
)
62 def connect_toggle(toggle
, fn
):
63 """Connect a toggle button to a function"""
64 toggle
.toggled
.connect(fn
)
68 """Return the active window for the current application"""
69 return QtWidgets
.QApplication
.activeWindow()
72 def hbox(margin
, spacing
, *items
):
73 """Create an HBoxLayout with the specified sizes and items"""
74 return box(QtWidgets
.QHBoxLayout
, margin
, spacing
, *items
)
77 def vbox(margin
, spacing
, *items
):
78 """Create a VBoxLayout with the specified sizes and items"""
79 return box(QtWidgets
.QVBoxLayout
, margin
, spacing
, *items
)
82 def buttongroup(*items
):
83 """Create a QButtonGroup for the specified items"""
84 group
= QtWidgets
.QButtonGroup()
90 def set_margin(layout
, margin
):
91 """Set the content margins for a layout"""
92 layout
.setContentsMargins(margin
, margin
, margin
, margin
)
95 def box(cls
, margin
, spacing
, *items
):
96 """Create a QBoxLayout with the specified sizes and items"""
100 layout
.setSpacing(spacing
)
101 set_margin(layout
, margin
)
104 if isinstance(i
, QtWidgets
.QWidget
):
106 elif isinstance(i
, (QtWidgets
.QHBoxLayout
, QtWidgets
.QVBoxLayout
,
107 QtWidgets
.QFormLayout
, QtWidgets
.QLayout
)):
113 elif isinstance(i
, int_types
):
119 def form(margin
, spacing
, *widgets
):
120 """Create a QFormLayout with the specified sizes and items"""
121 layout
= QtWidgets
.QFormLayout()
122 layout
.setSpacing(spacing
)
123 layout
.setFieldGrowthPolicy(QtWidgets
.QFormLayout
.ExpandingFieldsGrow
)
124 set_margin(layout
, margin
)
126 for idx
, (name
, widget
) in enumerate(widgets
):
127 if isinstance(name
, (str, ustr
)):
128 layout
.addRow(name
, widget
)
130 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.LabelRole
, name
)
131 layout
.setWidget(idx
, QtWidgets
.QFormLayout
.FieldRole
, widget
)
136 def grid(margin
, spacing
, *widgets
):
137 """Create a QGridLayout with the specified sizes and items"""
138 layout
= QtWidgets
.QGridLayout()
139 layout
.setSpacing(spacing
)
140 set_margin(layout
, margin
)
144 if isinstance(item
, QtWidgets
.QWidget
):
145 layout
.addWidget(*row
)
146 elif isinstance(item
, QtWidgets
.QLayoutItem
):
152 def splitter(orientation
, *widgets
):
153 """Create a spliter over the specified widgets
155 :param orientation: Qt.Horizontal or Qt.Vertical
158 layout
= QtWidgets
.QSplitter()
159 layout
.setOrientation(orientation
)
160 layout
.setHandleWidth(defs
.handle_width
)
161 layout
.setChildrenCollapsible(True)
163 for idx
, widget
in enumerate(widgets
):
164 layout
.addWidget(widget
)
165 layout
.setStretchFactor(idx
, 1)
167 # Workaround for Qt not setting the WA_Hover property for QSplitter
168 # Cf. https://bugreports.qt.io/browse/QTBUG-13768
169 layout
.handle(1).setAttribute(Qt
.WA_Hover
)
174 def label(text
=None, align
=None, fmt
=None, selectable
=True):
175 """Create a QLabel with the specified properties"""
176 widget
= QtWidgets
.QLabel()
177 if align
is not None:
178 widget
.setAlignment(align
)
180 widget
.setTextFormat(fmt
)
182 widget
.setTextInteractionFlags(Qt
.TextBrowserInteraction
)
183 widget
.setOpenExternalLinks(True)
189 def textbrowser(text
=None):
190 """Create a QTextBrowser for the specified text"""
191 widget
= QtWidgets
.QTextBrowser()
192 widget
.setOpenExternalLinks(True)
198 def add_completer(widget
, items
):
199 """Add simple completion to a widget"""
200 completer
= QtWidgets
.QCompleter(items
, widget
)
201 completer
.setCaseSensitivity(Qt
.CaseInsensitive
)
202 completer
.setCompletionMode(QtWidgets
.QCompleter
.InlineCompletion
)
203 widget
.setCompleter(completer
)
206 def prompt(msg
, title
=None, text
=''):
207 """Presents the user with an input widget and returns the input."""
210 result
= QtWidgets
.QInputDialog
.getText(
211 active_window(), msg
, title
,
212 QtWidgets
.QLineEdit
.Normal
, text
)
213 return (result
[0], result
[1])
216 def prompt_n(msg
, inputs
):
217 """Presents the user with N input widgets and returns the results"""
218 dialog
= QtWidgets
.QDialog(active_window())
219 dialog
.setWindowModality(Qt
.WindowModal
)
220 dialog
.setWindowTitle(msg
)
224 if len(k
+ v
) > len(long_value
):
227 metrics
= QtGui
.QFontMetrics(dialog
.font())
228 min_width
= metrics
.width(long_value
) + 100
231 dialog
.setMinimumWidth(min_width
)
233 ok_b
= ok_button(msg
, enabled
=False)
234 close_b
= close_button()
239 return [pair
[1].text().strip() for pair
in form_widgets
]
241 for name
, value
in inputs
:
242 lineedit
= QtWidgets
.QLineEdit()
243 # Enable the OK button only when all fields have been populated
244 lineedit
.textChanged
.connect(
245 lambda x
: ok_b
.setEnabled(all(get_values())))
247 lineedit
.setText(value
)
248 form_widgets
.append((name
, lineedit
))
251 form_layout
= form(defs
.no_margin
, defs
.button_spacing
, *form_widgets
)
252 button_layout
= hbox(defs
.no_margin
, defs
.button_spacing
,
253 STRETCH
, close_b
, ok_b
)
254 main_layout
= vbox(defs
.margin
, defs
.button_spacing
,
255 form_layout
, button_layout
)
256 dialog
.setLayout(main_layout
)
259 connect_button(ok_b
, dialog
.accept
)
260 connect_button(close_b
, dialog
.reject
)
262 accepted
= dialog
.exec_() == QtWidgets
.QDialog
.Accepted
264 ok
= accepted
and all(text
)
268 class TreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
270 TYPE
= QtGui
.QStandardItem
.UserType
+ 101
272 def __init__(self
, path
, icon
, deleted
):
273 QtWidgets
.QTreeWidgetItem
.__init
__(self
)
275 self
.deleted
= deleted
276 self
.setIcon(0, icons
.from_name(icon
))
277 self
.setText(0, path
)
283 def paths_from_indexes(model
, indexes
,
284 item_type
=TreeWidgetItem
.TYPE
,
286 """Return paths from a list of QStandardItemModel indexes"""
287 items
= [model
.itemFromIndex(i
) for i
in indexes
]
288 return paths_from_items(items
, item_type
=item_type
, item_filter
=item_filter
)
295 def paths_from_items(items
,
296 item_type
=TreeWidgetItem
.TYPE
,
298 """Return a list of paths from a list of items"""
299 if item_filter
is None:
300 item_filter
= _true_filter
301 return [i
.path
for i
in items
302 if i
.type() == item_type
and item_filter(i
)]
305 def confirm(title
, text
, informative_text
, ok_text
,
306 icon
=None, default
=True,
307 cancel_text
=None, cancel_icon
=None):
308 """Confirm that an action should take place"""
309 msgbox
= QtWidgets
.QMessageBox(active_window())
310 msgbox
.setWindowModality(Qt
.WindowModal
)
311 msgbox
.setWindowTitle(title
)
313 msgbox
.setInformativeText(informative_text
)
315 icon
= icons
.mkicon(icon
, icons
.ok
)
316 ok
= msgbox
.addButton(ok_text
, QtWidgets
.QMessageBox
.ActionRole
)
319 cancel
= msgbox
.addButton(QtWidgets
.QMessageBox
.Cancel
)
320 cancel_icon
= icons
.mkicon(cancel_icon
, icons
.close
)
321 cancel
.setIcon(cancel_icon
)
323 cancel
.setText(cancel_text
)
326 msgbox
.setDefaultButton(ok
)
328 msgbox
.setDefaultButton(cancel
)
330 return msgbox
.clickedButton() == ok
333 class ResizeableMessageBox(QtWidgets
.QMessageBox
):
335 def __init__(self
, parent
):
336 QtWidgets
.QMessageBox
.__init
__(self
, parent
)
337 self
.setMouseTracking(True)
338 self
.setSizeGripEnabled(True)
340 def event(self
, event
):
341 res
= QtWidgets
.QMessageBox
.event(self
, event
)
342 event_type
= event
.type()
343 if (event_type
== QtCore
.QEvent
.MouseMove
or
344 event_type
== QtCore
.QEvent
.MouseButtonPress
):
345 maxi
= QtCore
.QSize(defs
.max_size
, defs
.max_size
)
346 self
.setMaximumSize(maxi
)
347 text
= self
.findChild(QtWidgets
.QTextEdit
)
349 expand
= QtWidgets
.QSizePolicy
.Expanding
350 text
.setSizePolicy(QtWidgets
.QSizePolicy(expand
, expand
))
351 text
.setMaximumSize(maxi
)
355 def critical(title
, message
=None, details
=None):
356 """Show a warning with the provided title and message."""
359 mbox
= ResizeableMessageBox(active_window())
360 mbox
.setWindowTitle(title
)
361 mbox
.setTextFormat(Qt
.PlainText
)
362 mbox
.setText(message
)
363 mbox
.setIcon(QtWidgets
.QMessageBox
.Critical
)
364 mbox
.setStandardButtons(QtWidgets
.QMessageBox
.Close
)
365 mbox
.setDefaultButton(QtWidgets
.QMessageBox
.Close
)
367 mbox
.setDetailedText(details
)
371 def information(title
, message
=None, details
=None, informative_text
=None):
372 """Show information with the provided title and message."""
375 mbox
= QtWidgets
.QMessageBox(active_window())
376 mbox
.setStandardButtons(QtWidgets
.QMessageBox
.Close
)
377 mbox
.setDefaultButton(QtWidgets
.QMessageBox
.Close
)
378 mbox
.setWindowTitle(title
)
379 mbox
.setWindowModality(Qt
.WindowModal
)
380 mbox
.setTextFormat(Qt
.PlainText
)
381 mbox
.setText(message
)
383 mbox
.setInformativeText(informative_text
)
385 mbox
.setDetailedText(details
)
386 # Render into a 1-inch wide pixmap
387 pixmap
= icons
.cola().pixmap(defs
.large_icon
)
388 mbox
.setIconPixmap(pixmap
)
392 def question(title
, msg
, default
=True):
393 """Launches a QMessageBox question with the provided title and message.
394 Passing "default=False" will make "No" the default choice."""
395 yes
= QtWidgets
.QMessageBox
.Yes
396 no
= QtWidgets
.QMessageBox
.No
403 parent
= active_window()
404 MessageBox
= QtWidgets
.QMessageBox
405 result
= MessageBox
.question(parent
, title
, msg
, buttons
, default
)
406 return result
== QtWidgets
.QMessageBox
.Yes
409 def tree_selection(tree_item
, items
):
410 """Returns an array of model items that correspond to the selected
411 QTreeWidgetItem children"""
413 count
= min(tree_item
.childCount(), len(items
))
414 for idx
in range(count
):
415 if tree_item
.child(idx
).isSelected():
416 selected
.append(items
[idx
])
421 def tree_selection_items(tree_item
):
422 """Returns selected widget items"""
424 for idx
in range(tree_item
.childCount()):
425 child
= tree_item
.child(idx
)
426 if child
.isSelected():
427 selected
.append(child
)
432 def selected_item(list_widget
, items
):
433 """Returns the model item that corresponds to the selected QListWidget
435 widget_items
= list_widget
.selectedItems()
438 widget_item
= widget_items
[0]
439 row
= list_widget
.row(widget_item
)
446 def selected_items(list_widget
, items
):
447 """Returns an array of model items that correspond to the selected
449 item_count
= len(items
)
451 for widget_item
in list_widget
.selectedItems():
452 row
= list_widget
.row(widget_item
)
454 selected
.append(items
[row
])
458 def open_file(title
, directory
=None):
459 """Creates an Open File dialog and returns a filename."""
460 result
= compat
.getopenfilename(parent
=active_window(),
466 def open_files(title
, directory
=None, filters
=''):
467 """Creates an Open File dialog and returns a list of filenames."""
468 result
= compat
.getopenfilenames(parent
=active_window(),
475 def opendir_dialog(caption
, path
):
476 """Prompts for a directory path"""
478 options
= (QtWidgets
.QFileDialog
.ShowDirsOnly |
479 QtWidgets
.QFileDialog
.DontResolveSymlinks
)
480 return compat
.getexistingdirectory(parent
=active_window(),
486 def save_as(filename
, title
='Save As...'):
487 """Creates a Save File dialog and returns a filename."""
488 result
= compat
.getsavefilename(parent
=active_window(),
494 def copy_path(filename
, absolute
=True):
495 """Copy a filename to the clipboard"""
499 filename
= core
.abspath(filename
)
500 set_clipboard(filename
)
503 def set_clipboard(text
):
504 """Sets the copy/paste buffer to text."""
507 clipboard
= QtWidgets
.QApplication
.clipboard()
508 clipboard
.setText(text
, QtGui
.QClipboard
.Clipboard
)
509 clipboard
.setText(text
, QtGui
.QClipboard
.Selection
)
513 def persist_clipboard():
514 """Persist the clipboard
516 X11 stores only a reference to the clipboard data.
517 Send a clipboard event to force a copy of the clipboard to occur.
518 This ensures that the clipboard is present after git-cola exits.
519 Otherwise, the reference is destroyed on exit.
521 C.f. https://stackoverflow.com/questions/2007103/how-can-i-disable-clear-of-clipboard-on-exit-of-pyqt4-application
524 clipboard
= QtWidgets
.QApplication
.clipboard()
525 event
= QtCore
.QEvent(QtCore
.QEvent
.Clipboard
)
526 QtWidgets
.QApplication
.sendEvent(clipboard
, event
)
529 def add_action_bool(widget
, text
, fn
, checked
, *shortcuts
):
531 action
= _add_action(widget
, text
, tip
, fn
, connect_action_bool
, *shortcuts
)
532 action
.setCheckable(True)
533 action
.setChecked(checked
)
537 def add_action(widget
, text
, fn
, *shortcuts
):
539 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
542 def add_action_with_status_tip(widget
, text
, tip
, fn
, *shortcuts
):
543 return _add_action(widget
, text
, tip
, fn
, connect_action
, *shortcuts
)
546 def _add_action(widget
, text
, tip
, fn
, connect
, *shortcuts
):
547 action
= QtWidgets
.QAction(text
, widget
)
548 if hasattr(action
, 'setIconVisibleInMenu'):
549 action
.setIconVisibleInMenu(True)
551 action
.setStatusTip(tip
)
554 action
.setShortcuts(shortcuts
)
555 if hasattr(Qt
, 'WidgetWithChildrenShortcut'):
556 action
.setShortcutContext(Qt
.WidgetWithChildrenShortcut
)
557 widget
.addAction(action
)
561 def set_selected_item(widget
, idx
):
562 """Sets a the currently selected item to the item at index idx."""
563 if type(widget
) is QtWidgets
.QTreeWidget
:
564 item
= widget
.topLevelItem(idx
)
566 item
.setSelected(True)
567 widget
.setCurrentItem(item
)
570 def add_items(widget
, items
):
571 """Adds items to a widget."""
578 def set_items(widget
, items
):
579 """Clear the existing widget contents and set the new items."""
581 add_items(widget
, items
)
584 def create_treeitem(filename
, staged
=False, deleted
=False, untracked
=False):
585 """Given a filename, return a TreeWidgetItem for a status widget
587 "staged", "deleted, and "untracked" control which icon is used.
590 icon_name
= icons
.status(filename
, deleted
, staged
, untracked
)
591 return TreeWidgetItem(filename
, icons
.name_from_basename(icon_name
),
595 def add_close_action(widget
):
596 """Adds close action and shortcuts to a widget."""
597 return add_action(widget
, N_('Close...'),
598 widget
.close
, hotkeys
.CLOSE
, hotkeys
.QUIT
)
602 """Return the current application"""
603 return QtWidgets
.QApplication
.instance()
607 """Return the desktop"""
608 return app().desktop()
611 def center_on_screen(widget
):
612 """Move widget to the center of the default screen"""
614 rect
= desk
.screenGeometry(QtGui
.QCursor().pos())
615 cy
= rect
.height()//2
617 widget
.move(cx
- widget
.width()//2, cy
- widget
.height()//2)
620 def default_size(parent
, width
, height
, use_parent_height
=True):
621 """Return the parent's size, or the provided defaults"""
622 if parent
is not None:
623 width
= parent
.width()
624 if use_parent_height
:
625 height
= parent
.height()
626 return (width
, height
)
629 def default_monospace_font():
632 if utils
.is_darwin():
634 font
.setFamily(family
)
639 font_str
= gitcfg
.current().get(prefs
.FONTDIFF
)
641 font_str
= default_monospace_font().toString()
646 return font(diff_font_str())
651 font
.fromString(string
)
655 def create_button(text
='', layout
=None, tooltip
=None, icon
=None,
656 enabled
=True, default
=False):
657 """Create a button, set its title, and add it to the parent."""
658 button
= QtWidgets
.QPushButton()
659 button
.setCursor(Qt
.PointingHandCursor
)
664 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
665 if tooltip
is not None:
666 button
.setToolTip(tooltip
)
667 if layout
is not None:
668 layout
.addWidget(button
)
670 button
.setEnabled(False)
672 button
.setDefault(True)
676 def create_action_button(tooltip
=None, icon
=None):
677 button
= QtWidgets
.QPushButton()
678 button
.setCursor(Qt
.PointingHandCursor
)
680 if tooltip
is not None:
681 button
.setToolTip(tooltip
)
684 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
688 def ok_button(text
, default
=True, enabled
=True, icon
=None):
691 return create_button(text
=text
, icon
=icon
, default
=default
, enabled
=enabled
)
695 return create_button(text
=N_('Close'), icon
=icons
.close())
698 def edit_button(enabled
=True, default
=False):
699 return create_button(text
=N_('Edit'), icon
=icons
.edit(),
700 enabled
=enabled
, default
=default
)
703 def refresh_button(enabled
=True, default
=False):
704 return create_button(text
=N_('Refresh'), icon
=icons
.sync(),
705 enabled
=enabled
, default
=default
)
708 def hide_button_menu_indicator(button
):
709 """Hide the menu indicator icon on buttons"""
711 name
= button
.__class
__.__name
__
713 %(name)s::menu-indicator {
717 if name
== 'QPushButton':
723 button
.setStyleSheet(stylesheet
% dict(name
=name
))
726 def checkbox(text
='', tooltip
='', checked
=None):
727 """Create a checkbox"""
728 return _checkbox(QtWidgets
.QCheckBox
, text
, tooltip
, checked
)
731 def radio(text
='', tooltip
='', checked
=None):
732 """Create a radio button"""
733 return _checkbox(QtWidgets
.QRadioButton
, text
, tooltip
, checked
)
736 def _checkbox(cls
, text
, tooltip
, checked
):
737 """Create a widget and apply properties"""
742 widget
.setToolTip(tooltip
)
743 if checked
is not None:
744 widget
.setChecked(checked
)
748 class DockTitleBarWidget(QtWidgets
.QWidget
):
750 def __init__(self
, parent
, title
, stretch
=True):
751 QtWidgets
.QWidget
.__init
__(self
, parent
)
752 self
.setAutoFillBackground(True)
753 self
.label
= qlabel
= QtWidgets
.QLabel(title
, self
)
757 qlabel
.setCursor(Qt
.OpenHandCursor
)
759 self
.close_button
= create_action_button(
760 tooltip
=N_('Close'), icon
=icons
.close())
762 self
.toggle_button
= create_action_button(
763 tooltip
=N_('Detach'), icon
=icons
.external())
765 self
.corner_layout
= hbox(defs
.no_margin
, defs
.spacing
)
772 self
.main_layout
= hbox(defs
.small_margin
, defs
.spacing
,
773 qlabel
, separator
, self
.corner_layout
,
774 self
.toggle_button
, self
.close_button
)
775 self
.setLayout(self
.main_layout
)
777 connect_button(self
.toggle_button
, self
.toggle_floating
)
778 connect_button(self
.close_button
, self
.toggle_visibility
)
780 def toggle_floating(self
):
781 self
.parent().setFloating(not self
.parent().isFloating())
782 self
.update_tooltips()
784 def toggle_visibility(self
):
785 self
.parent().toggleViewAction().trigger()
787 def set_title(self
, title
):
788 self
.label
.setText(title
)
790 def add_corner_widget(self
, widget
):
791 self
.corner_layout
.addWidget(widget
)
793 def update_tooltips(self
):
794 if self
.parent().isFloating():
795 tooltip
= N_('Attach')
797 tooltip
= N_('Detach')
798 self
.toggle_button
.setToolTip(tooltip
)
801 def create_dock(title
, parent
, stretch
=True):
802 """Create a dock widget and set it up accordingly."""
803 dock
= QtWidgets
.QDockWidget(parent
)
804 dock
.setWindowTitle(title
)
805 dock
.setObjectName(title
)
806 titlebar
= DockTitleBarWidget(dock
, title
, stretch
=stretch
)
807 dock
.setTitleBarWidget(titlebar
)
808 dock
.setAutoFillBackground(True)
809 if hasattr(parent
, 'dockwidgets'):
810 parent
.dockwidgets
.append(dock
)
814 def create_menu(title
, parent
):
815 """Create a menu and set its title."""
816 qmenu
= QtWidgets
.QMenu(title
, parent
)
820 def create_toolbutton(text
=None, layout
=None, tooltip
=None, icon
=None):
821 button
= QtWidgets
.QToolButton()
822 button
.setAutoRaise(True)
823 button
.setAutoFillBackground(True)
824 button
.setCursor(Qt
.PointingHandCursor
)
827 button
.setIconSize(QtCore
.QSize(defs
.small_icon
, defs
.small_icon
))
830 button
.setToolButtonStyle(Qt
.ToolButtonTextBesideIcon
)
831 if tooltip
is not None:
832 button
.setToolTip(tooltip
)
833 if layout
is not None:
834 layout
.addWidget(button
)
838 def mimedata_from_paths(paths
):
839 """Return mimedata with a list of absolute path URLs"""
841 abspaths
= [core
.abspath(path
) for path
in paths
]
842 urls
= [QtCore
.QUrl
.fromLocalFile(path
) for path
in abspaths
]
844 mimedata
= QtCore
.QMimeData()
845 mimedata
.setUrls(urls
)
847 # The text/x-moz-list format is always included by Qt, and doing
848 # mimedata.removeFormat('text/x-moz-url') has no effect.
849 # C.f. http://www.qtcentre.org/threads/44643-Dragging-text-uri-list-Qt-inserts-garbage
851 # gnome-terminal expects utf-16 encoded text, but other terminals,
852 # e.g. terminator, prefer utf-8, so allow cola.dragencoding
853 # to override the default.
854 paths_text
= core
.list2cmdline(abspaths
)
855 encoding
= gitcfg
.current().get('cola.dragencoding', 'utf-16')
856 moz_text
= core
.encode(paths_text
, encoding
=encoding
)
857 mimedata
.setData('text/x-moz-url', moz_text
)
862 def path_mimetypes():
863 return ['text/uri-list', 'text/x-moz-url']
866 class BlockSignals(object):
867 """Context manager for blocking a signals on a widget"""
869 def __init__(self
, *widgets
):
870 self
.widgets
= widgets
874 for w
in self
.widgets
:
875 self
.values
[w
] = w
.blockSignals(True)
878 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
879 for w
in self
.widgets
:
880 w
.blockSignals(self
.values
[w
])
883 class Channel(QtCore
.QObject
):
884 finished
= Signal(object)
885 result
= Signal(object)
888 class Task(QtCore
.QRunnable
):
889 """Disable auto-deletion to avoid gc issues
891 Python's garbage collector will try to double-free the task
892 once it's finished, so disable Qt's auto-deletion as a workaround.
896 def __init__(self
, parent
):
897 QtCore
.QRunnable
.__init
__(self
)
899 self
.channel
= Channel()
901 self
.setAutoDelete(False)
904 self
.result
= self
.task()
905 self
.channel
.result
.emit(self
.result
)
912 self
.channel
.finished
.emit(self
)
914 def connect(self
, handler
):
915 self
.channel
.result
.connect(handler
, type=Qt
.QueuedConnection
)
918 class SimpleTask(Task
):
919 """Run a simple callable as a task"""
921 def __init__(self
, parent
, fn
, *args
, **kwargs
):
922 Task
.__init
__(self
, parent
)
929 return self
.fn(*self
.args
, **self
.kwargs
)
932 class RunTask(QtCore
.QObject
):
933 """Runs QRunnable instances and transfers control when they finish"""
935 def __init__(self
, parent
=None):
936 QtCore
.QObject
.__init
__(self
, parent
)
938 self
.task_details
= {}
939 self
.threadpool
= QtCore
.QThreadPool
.globalInstance()
941 def start(self
, task
, progress
=None, finish
=None):
942 """Start the task and register a callback"""
943 if progress
is not None:
945 # prevents garbage collection bugs in certain PyQt4 versions
946 self
.tasks
.append(task
)
948 self
.task_details
[task_id
] = (progress
, finish
)
950 task
.channel
.finished
.connect(self
.finish
, type=Qt
.QueuedConnection
)
951 self
.threadpool
.start(task
)
953 def finish(self
, task
):
956 self
.tasks
.remove(task
)
960 progress
, finish
= self
.task_details
[task_id
]
961 del self
.task_details
[task_id
]
963 finish
= progress
= None
965 if progress
is not None:
968 if finish
is not None:
972 # Syntax highlighting
975 color
= QtGui
.QColor()
976 color
.setRgb(r
, g
, b
)
980 def rgba(r
, g
, b
, a
=255):
991 """Convert a QColor into an rgb(int, int, int) CSS string"""
992 return 'rgb(%d, %d, %d)' % (color
.red(), color
.green(), color
.blue())
996 """Convert a QColor into a hex aabbcc string"""
997 return '%x%x%x' % (color
.red(), color
.green(), color
.blue())
1000 def make_format(fg
=None, bg
=None, bold
=False):
1001 fmt
= QtGui
.QTextCharFormat()
1003 fmt
.setForeground(fg
)
1005 fmt
.setBackground(bg
)
1007 fmt
.setFontWeight(QtGui
.QFont
.Bold
)
1012 Interaction
.critical
= staticmethod(critical
)
1013 Interaction
.confirm
= staticmethod(confirm
)
1014 Interaction
.question
= staticmethod(question
)
1015 Interaction
.information
= staticmethod(information
)