1 from __future__
import absolute_import
, division
, print_function
, unicode_literals
3 from functools
import partial
5 from qtpy
import QtCore
7 from qtpy
import QtWidgets
8 from qtpy
.QtCore
import Qt
9 from qtpy
.QtCore
import Signal
10 from qtpy
.QtWidgets
import QDockWidget
13 from ..interaction
import Interaction
14 from ..settings
import Settings
, mklist
15 from ..models
import prefs
17 from .. import hotkeys
19 from .. import qtcompat
20 from .. import qtutils
25 class WidgetMixin(object):
26 """Mix-in for common utilities and serialization of widget state"""
28 closed
= Signal(QtWidgets
.QWidget
)
31 self
._unmaximized
_rect
= {}
34 parent
= self
.parent()
38 width
= parent
.width()
39 center_x
= left
+ width
// 2
40 x
= center_x
- self
.width() // 2
45 def resize_to_desktop(self
):
46 desktop
= QtWidgets
.QApplication
.instance().desktop()
47 width
= desktop
.width()
48 height
= desktop
.height()
50 self
.resize(width
, height
)
52 shown
= self
.isVisible()
53 # earlier show() fools Windows focus stealing prevention. the main
54 # window is blocked for the duration of "git rebase" and we don't
55 # want to present a blocked window with git-cola-sequence-editor
58 self
.setWindowState(Qt
.WindowMaximized
)
63 """Returns the name of the view class"""
64 return self
.__class
__.__name
__.lower()
66 def save_state(self
, settings
=None):
68 context
= getattr(self
, 'context', None)
71 save
= cfg
.get('cola.savewindowsettings', default
=True)
74 settings
= Settings
.read()
75 settings
.save_gui_state(self
)
77 def restore_state(self
, settings
=None):
79 settings
= Settings
.read()
80 state
= settings
.get_gui_state(self
)
82 result
= self
.apply_state(state
)
87 def apply_state(self
, state
):
88 """Imports data for view save/restore"""
90 width
= utils
.asint(state
.get('width'))
91 height
= utils
.asint(state
.get('height'))
92 x
= utils
.asint(state
.get('x'))
93 y
= utils
.asint(state
.get('y'))
95 geometry
= state
.get('geometry', '')
97 from_base64
= QtCore
.QByteArray
.fromBase64
98 result
= self
.restoreGeometry(from_base64(core
.encode(geometry
)))
99 elif width
and height
:
100 # Users migrating from older versions won't have 'geometry'.
101 # They'll be upgraded to the new format on shutdown.
102 self
.resize(width
, height
)
109 def export_state(self
):
110 """Exports data for view save/restore"""
112 geometry
= self
.saveGeometry()
113 state
['geometry'] = geometry
.toBase64().data().decode('ascii')
114 # Until 2020: co-exist with older versions
115 state
['width'] = self
.width()
116 state
['height'] = self
.height()
117 state
['x'] = self
.x()
118 state
['y'] = self
.y()
121 def save_settings(self
, settings
=None):
122 return self
.save_state(settings
=settings
)
124 def closeEvent(self
, event
):
126 self
.closed
.emit(self
)
127 self
.Base
.closeEvent(self
, event
)
129 def init_size(self
, parent
=None, settings
=None, width
=0, height
=0):
131 width
= defs
.dialog_w
133 height
= defs
.dialog_h
134 self
.init_state(settings
, self
.resize_to_parent
, parent
, width
, height
)
136 def init_state(self
, settings
, callback
, *args
, **kwargs
):
137 """Restore saved settings or set the initial location"""
138 if not self
.restore_state(settings
=settings
):
139 callback(*args
, **kwargs
)
142 def resize_to_parent(self
, parent
, w
, h
):
143 """Set the initial size of the widget"""
144 width
, height
= qtutils
.default_size(parent
, w
, h
)
145 self
.resize(width
, height
)
148 class MainWindowMixin(WidgetMixin
):
150 WidgetMixin
.__init
__(self
)
152 self
.dockwidgets
= []
153 self
.lock_layout
= False
154 self
.widget_version
= 0
155 qtcompat
.set_common_dock_options(self
)
156 self
.default_state
= None
158 def init_state(self
, settings
, callback
, *args
, **kwargs
):
159 """Save the initial state before calling the parent initializer"""
160 self
.default_state
= self
.saveState(self
.widget_version
)
161 super(MainWindowMixin
, self
).init_state(settings
, callback
, *args
, **kwargs
)
163 def export_state(self
):
164 """Exports data for save/restore"""
165 state
= WidgetMixin
.export_state(self
)
166 windowstate
= self
.saveState(self
.widget_version
)
167 state
['lock_layout'] = self
.lock_layout
168 state
['windowstate'] = windowstate
.toBase64().data().decode('ascii')
171 def save_settings(self
, settings
=None):
173 context
= getattr(self
, 'context', None)
175 settings
= Settings
.read()
177 settings
= context
.settings
179 settings
.add_recent(core
.getcwd(), prefs
.maxrecent(context
))
180 return WidgetMixin
.save_settings(self
, settings
=settings
)
182 def apply_state(self
, state
):
183 result
= WidgetMixin
.apply_state(self
, state
)
184 windowstate
= state
.get('windowstate', '')
186 from_base64
= QtCore
.QByteArray
.fromBase64
189 from_base64(core
.encode(windowstate
)), self
.widget_version
196 self
.lock_layout
= state
.get('lock_layout', self
.lock_layout
)
197 self
.update_dockwidget_lock_state()
198 self
.update_dockwidget_tooltips()
202 def reset_layout(self
):
203 self
.restoreState(self
.default_state
, self
.widget_version
)
205 def set_lock_layout(self
, lock_layout
):
206 self
.lock_layout
= lock_layout
207 self
.update_dockwidget_lock_state()
209 def update_dockwidget_lock_state(self
):
211 features
= QDockWidget
.DockWidgetClosable | QDockWidget
.DockWidgetFloatable
214 QDockWidget
.DockWidgetClosable
215 | QDockWidget
.DockWidgetFloatable
216 | QDockWidget
.DockWidgetMovable
218 for widget
in self
.dockwidgets
:
219 widget
.titleBarWidget().update_tooltips()
220 widget
.setFeatures(features
)
222 def update_dockwidget_tooltips(self
):
223 for widget
in self
.dockwidgets
:
224 widget
.titleBarWidget().update_tooltips()
227 # pylint: disable=too-many-ancestors
228 class ListWidget(QtWidgets
.QListWidget
):
229 """QListWidget with vim j/k navigation hotkeys"""
231 def __init__(self
, parent
=None):
232 super(ListWidget
, self
).__init
__(parent
)
234 self
.up_action
= qtutils
.add_action(
239 hotkeys
.MOVE_UP_SECONDARY
,
242 self
.down_action
= qtutils
.add_action(
247 hotkeys
.MOVE_DOWN_SECONDARY
,
250 def selected_item(self
):
251 return self
.currentItem()
253 def selected_items(self
):
254 return self
.selectedItems()
262 def move(self
, direction
):
263 item
= self
.selected_item()
265 row
= (self
.row(item
) + direction
) % self
.count()
266 elif self
.count() > 0:
267 row
= (self
.count() + direction
) % self
.count()
270 new_item
= self
.item(row
)
272 self
.setCurrentItem(new_item
)
275 class TreeMixin(object):
276 def __init__(self
, widget
, Base
):
280 widget
.setAlternatingRowColors(True)
281 widget
.setUniformRowHeights(True)
282 widget
.setAllColumnsShowFocus(True)
283 widget
.setAnimated(True)
284 widget
.setRootIsDecorated(False)
286 def keyPressEvent(self
, event
):
288 Make LeftArrow to work on non-directories.
290 When LeftArrow is pressed on a file entry or an unexpanded
291 directory, then move the current index to the parent directory.
293 This simplifies navigation using the keyboard.
294 For power-users, we support Vim keybindings ;-P
297 # Check whether the item is expanded before calling the base class
298 # keyPressEvent otherwise we end up collapsing and changing the
299 # current index in one shot, which we don't want to do.
301 index
= widget
.currentIndex()
302 was_expanded
= widget
.isExpanded(index
)
303 was_collapsed
= not was_expanded
306 # Rewrite the event before marshalling to QTreeView.event()
309 # Remap 'H' to 'Left'
311 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Left
, event
.modifiers())
312 # Remap 'J' to 'Down'
313 elif key
== Qt
.Key_J
:
314 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Down
, event
.modifiers())
316 elif key
== Qt
.Key_K
:
317 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Up
, event
.modifiers())
318 # Remap 'L' to 'Right'
319 elif key
== Qt
.Key_L
:
320 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Right
, event
.modifiers())
322 # Re-read the event key to take the remappings into account
325 idxs
= widget
.selectedIndexes()
326 rows
= [idx
.row() for idx
in idxs
]
327 if len(rows
) == 1 and rows
[0] == 0:
328 # The cursor is at the beginning of the line.
329 # If we have selection then simply reset the cursor.
330 # Otherwise, emit a signal so that the parent can
334 elif key
== Qt
.Key_Space
:
337 result
= self
.Base
.keyPressEvent(widget
, event
)
339 # Let others hook in here before we change the indexes
340 widget
.index_about_to_change
.emit()
342 # Automatically select the first entry when expanding a directory
343 if key
== Qt
.Key_Right
and was_collapsed
and widget
.isExpanded(index
):
344 index
= widget
.moveCursor(widget
.MoveDown
, event
.modifiers())
345 widget
.setCurrentIndex(index
)
347 # Process non-root entries with valid parents only.
348 elif key
== Qt
.Key_Left
and index
.parent().isValid():
350 # File entries have rowCount() == 0
351 model
= widget
.model()
353 hasattr(model
, 'itemFromIndex')
354 and model
.itemFromIndex(index
).rowCount() == 0
356 widget
.setCurrentIndex(index
.parent())
358 # Otherwise, do this for collapsed directories only
360 widget
.setCurrentIndex(index
.parent())
362 # If it's a movement key ensure we have a selection
363 elif key
in (Qt
.Key_Left
, Qt
.Key_Up
, Qt
.Key_Right
, Qt
.Key_Down
):
364 # Try to select the first item if the model index is invalid
365 item
= self
.selected_item()
366 if item
is None or not index
.isValid():
367 index
= widget
.model().index(0, 0, QtCore
.QModelIndex())
369 widget
.setCurrentIndex(index
)
373 def item_from_index(self
, item
):
374 """Return a QModelIndex from the provided item"""
375 if hasattr(self
, 'itemFromIndex'):
376 index
= self
.itemFromIndex(item
)
378 index
= self
.model().itemFromIndex()
382 root
= self
.widget
.invisibleRootItem()
384 count
= root
.childCount()
385 return [child(i
) for i
in range(count
)]
387 def selected_items(self
):
388 """Return all selected items"""
390 if hasattr(widget
, 'selectedItems'):
391 return widget
.selectedItems()
393 if hasattr(widget
, 'itemFromIndex'):
394 item_from_index
= widget
.itemFromIndex
396 item_from_index
= widget
.model().itemFromIndex
397 return [item_from_index(i
) for i
in widget
.selectedIndexes()]
399 def selected_item(self
):
400 """Return the first selected item"""
401 selected_items
= self
.selected_items()
402 if not selected_items
:
404 return selected_items
[0]
406 def current_item(self
):
409 if hasattr(widget
, 'currentItem'):
410 item
= widget
.currentItem()
412 index
= widget
.currentIndex()
414 item
= widget
.model().itemFromIndex(index
)
417 def column_widths(self
):
418 """Return the tree's column widths"""
420 count
= widget
.header().count()
421 return [widget
.columnWidth(i
) for i
in range(count
)]
423 def set_column_widths(self
, widths
):
424 """Set the tree's column widths"""
427 count
= widget
.header().count()
428 if len(widths
) > count
:
429 widths
= widths
[:count
]
430 for idx
, value
in enumerate(widths
):
431 widget
.setColumnWidth(idx
, value
)
434 class DraggableTreeMixin(TreeMixin
):
435 """A tree widget with internal drag+drop reordering of rows
437 Expects that the widget provides an `items_moved` signal.
441 def __init__(self
, widget
, Base
):
442 super(DraggableTreeMixin
, self
).__init
__(widget
, Base
)
444 self
._inner
_drag
= False
445 widget
.setAcceptDrops(True)
446 widget
.setSelectionMode(widget
.SingleSelection
)
447 widget
.setDragEnabled(True)
448 widget
.setDropIndicatorShown(True)
449 widget
.setDragDropMode(QtWidgets
.QAbstractItemView
.InternalMove
)
450 widget
.setSortingEnabled(False)
452 def dragEnterEvent(self
, event
):
453 """Accept internal drags only"""
455 self
.Base
.dragEnterEvent(widget
, event
)
456 self
._inner
_drag
= event
.source() == widget
458 event
.acceptProposedAction()
462 def dragLeaveEvent(self
, event
):
464 self
.Base
.dragLeaveEvent(widget
, event
)
469 self
._inner
_drag
= False
471 def dropEvent(self
, event
):
472 """Re-select selected items after an internal move"""
473 if not self
._inner
_drag
:
477 clicked_items
= self
.selected_items()
478 event
.setDropAction(Qt
.MoveAction
)
479 self
.Base
.dropEvent(widget
, event
)
482 widget
.clearSelection()
483 for item
in clicked_items
:
484 item
.setSelected(True)
485 widget
.items_moved
.emit(clicked_items
)
486 self
._inner
_drag
= False
487 event
.accept() # must be called after dropEvent()
489 def mousePressEvent(self
, event
):
490 """Clear the selection when a mouse click hits no item"""
492 clicked_item
= widget
.itemAt(event
.pos())
493 if clicked_item
is None:
494 widget
.clearSelection()
495 return self
.Base
.mousePressEvent(widget
, event
)
498 class Widget(WidgetMixin
, QtWidgets
.QWidget
):
499 Base
= QtWidgets
.QWidget
501 def __init__(self
, parent
=None):
502 QtWidgets
.QWidget
.__init
__(self
, parent
)
503 WidgetMixin
.__init
__(self
)
506 class Dialog(WidgetMixin
, QtWidgets
.QDialog
):
507 Base
= QtWidgets
.QDialog
509 def __init__(self
, parent
=None):
510 QtWidgets
.QDialog
.__init
__(self
, parent
)
511 WidgetMixin
.__init
__(self
)
512 # Disable the Help button hint on Windows
513 if hasattr(Qt
, 'WindowContextHelpButtonHint'):
514 help_hint
= Qt
.WindowContextHelpButtonHint
515 flags
= self
.windowFlags() & ~help_hint
516 self
.setWindowFlags(flags
)
521 return self
.Base
.accept(self
)
526 return self
.Base
.reject(self
)
528 # pylint: disable=no-self-use
530 """Extension method for model deregistration in sub-classes"""
534 """save_settings() is handled by accept() and reject()"""
536 self
.Base
.close(self
)
538 def closeEvent(self
, event
):
539 """save_settings() is handled by accept() and reject()"""
541 self
.Base
.closeEvent(self
, event
)
544 class MainWindow(MainWindowMixin
, QtWidgets
.QMainWindow
):
545 Base
= QtWidgets
.QMainWindow
547 def __init__(self
, parent
=None):
548 QtWidgets
.QMainWindow
.__init
__(self
, parent
)
549 MainWindowMixin
.__init
__(self
)
552 # pylint: disable=too-many-ancestors
553 class TreeView(QtWidgets
.QTreeView
):
558 index_about_to_change
= Signal()
560 def __init__(self
, parent
=None):
561 QtWidgets
.QTreeView
.__init
__(self
, parent
)
562 self
._mixin
= self
.Mixin(self
, QtWidgets
.QTreeView
)
564 def keyPressEvent(self
, event
):
565 return self
._mixin
.keyPressEvent(event
)
567 def current_item(self
):
568 return self
._mixin
.current_item()
570 def selected_item(self
):
571 return self
._mixin
.selected_item()
573 def selected_items(self
):
574 return self
._mixin
.selected_items()
577 return self
._mixin
.items()
579 def column_widths(self
):
580 return self
._mixin
.column_widths()
582 def set_column_widths(self
, widths
):
583 return self
._mixin
.set_column_widths(widths
)
586 # pylint: disable=too-many-ancestors
587 class TreeWidget(QtWidgets
.QTreeWidget
):
592 index_about_to_change
= Signal()
594 def __init__(self
, parent
=None):
595 super(TreeWidget
, self
).__init
__(parent
)
596 self
._mixin
= self
.Mixin(self
, QtWidgets
.QTreeWidget
)
598 def keyPressEvent(self
, event
):
599 return self
._mixin
.keyPressEvent(event
)
601 def current_item(self
):
602 return self
._mixin
.current_item()
604 def selected_item(self
):
605 return self
._mixin
.selected_item()
607 def selected_items(self
):
608 return self
._mixin
.selected_items()
611 return self
._mixin
.items()
613 def column_widths(self
):
614 return self
._mixin
.column_widths()
616 def set_column_widths(self
, widths
):
617 return self
._mixin
.set_column_widths(widths
)
620 # pylint: disable=too-many-ancestors
621 class DraggableTreeWidget(TreeWidget
):
622 Mixin
= DraggableTreeMixin
623 items_moved
= Signal(object)
625 def mousePressEvent(self
, event
):
626 return self
._mixin
.mousePressEvent(event
)
628 def dropEvent(self
, event
):
629 return self
._mixin
.dropEvent(event
)
631 def dragLeaveEvent(self
, event
):
632 return self
._mixin
.dragLeaveEvent(event
)
634 def dragEnterEvent(self
, event
):
635 return self
._mixin
.dragEnterEvent(event
)
638 class ProgressDialog(QtWidgets
.QProgressDialog
):
639 """Custom progress dialog
641 This dialog ignores the ESC key so that it is not
644 A thread is spawned to animate the progress label text.
648 def __init__(self
, title
, label
, parent
):
649 QtWidgets
.QProgressDialog
.__init
__(self
, parent
)
650 if parent
is not None:
651 self
.setWindowModality(Qt
.WindowModal
)
654 self
.setMinimumDuration(0)
655 self
.setCancelButton(None)
656 self
.setFont(qtutils
.default_monospace_font())
657 self
.thread
= ProgressAnimationThread(label
, self
)
658 self
.thread
.updated
.connect(self
.refresh
, type=Qt
.QueuedConnection
)
660 self
.set_details(title
, label
)
662 def set_details(self
, title
, label
):
663 self
.setWindowTitle(title
)
664 self
.setLabelText(label
+ ' ')
665 self
.thread
.set_text(label
)
667 def refresh(self
, txt
):
668 self
.setLabelText(txt
)
670 def keyPressEvent(self
, event
):
671 if event
.key() != Qt
.Key_Escape
:
672 super(ProgressDialog
, self
).keyPressEvent(event
)
675 QtWidgets
.QApplication
.setOverrideCursor(Qt
.WaitCursor
)
676 super(ProgressDialog
, self
).show()
680 QtWidgets
.QApplication
.restoreOverrideCursor()
683 super(ProgressDialog
, self
).hide()
686 class ProgressAnimationThread(QtCore
.QThread
):
687 """Emits a pseudo-animated text stream for progress bars"""
689 updated
= Signal(object)
691 def __init__(self
, txt
, parent
, timeout
=0.1):
692 QtCore
.QThread
.__init
__(self
, parent
)
695 self
.timeout
= timeout
705 def set_text(self
, txt
):
709 self
.idx
= (self
.idx
+ 1) % len(self
.symbols
)
710 return self
.txt
+ self
.symbols
[self
.idx
]
718 self
.updated
.emit(self
.cycle())
719 time
.sleep(self
.timeout
)
722 class SpinBox(QtWidgets
.QSpinBox
):
724 self
, parent
=None, value
=None, mini
=1, maxi
=99999, step
=0, prefix
='', suffix
=''
726 QtWidgets
.QSpinBox
.__init
__(self
, parent
)
727 self
.setPrefix(prefix
)
728 self
.setSuffix(suffix
)
729 self
.setWrapping(True)
730 self
.setMinimum(mini
)
731 self
.setMaximum(maxi
)
733 self
.setSingleStep(step
)
734 if value
is not None:
738 metrics
= QtGui
.QFontMetrics(font
)
739 width
= max(self
.minimumWidth(), metrics
.width('XXXXXX'))
740 self
.setMinimumWidth(width
)
743 def export_header_columns(widget
, state
):
744 """Save QHeaderView column sizes"""
746 header
= widget
.horizontalHeader()
747 for idx
in range(header
.count()):
748 columns
.append(header
.sectionSize(idx
))
750 state
['columns'] = columns
753 def apply_header_columns(widget
, state
):
754 """Apply QHeaderView column sizes"""
755 columns
= mklist(state
.get('columns', []))
756 header
= widget
.horizontalHeader()
757 if header
.stretchLastSection():
758 # Setting the size will make the section wider than necessary, which
759 # defeats the purpose of the stretch flag. Skip the last column when
760 # it's stretchy so that it retains the stretchy behavior.
761 columns
= columns
[:-1]
762 for idx
, size
in enumerate(columns
):
763 header
.resizeSection(idx
, size
)
766 class MessageBox(Dialog
):
767 """Improved QMessageBox replacement
769 QMessageBox has a lot of usability issues. It sometimes cannot be
770 resized, and it brings along a lots of annoying properties that we'd have
771 to workaround, so we use a simple custom dialog instead.
790 Dialog
.__init
__(self
, parent
=parent
)
793 self
.setWindowModality(Qt
.WindowModal
)
795 self
.setWindowTitle(title
)
797 self
.logo_label
= QtWidgets
.QLabel()
799 # Render into a 1-inch wide pixmap
800 pixmap
= logo
.pixmap(defs
.large_icon
)
801 self
.logo_label
.setPixmap(pixmap
)
803 self
.logo_label
.hide()
805 self
.text_label
= QtWidgets
.QLabel()
806 self
.text_label
.setText(text
)
808 self
.info_label
= QtWidgets
.QLabel()
810 self
.info_label
.setText(info
)
812 self
.info_label
.hide()
814 ok_icon
= icons
.mkicon(ok_icon
, icons
.ok
)
815 self
.button_ok
= qtutils
.create_button(text
=ok_text
, icon
=ok_icon
)
817 self
.button_close
= qtutils
.close_button(text
=cancel_text
, icon
=cancel_icon
)
820 self
.button_ok
.setText(ok_text
)
822 self
.button_ok
.hide()
824 self
.details_text
= QtWidgets
.QPlainTextEdit()
825 self
.details_text
.setReadOnly(True)
827 self
.details_text
.setFont(qtutils
.default_monospace_font())
828 self
.details_text
.setPlainText(details
)
830 self
.details_text
.hide()
832 self
.info_layout
= qtutils
.vbox(
840 self
.top_layout
= qtutils
.hbox(
848 self
.buttons_layout
= qtutils
.hbox(
856 self
.main_layout
= qtutils
.vbox(
863 self
.main_layout
.setStretchFactor(self
.details_text
, 2)
864 self
.setLayout(self
.main_layout
)
867 self
.button_ok
.setDefault(True)
868 self
.button_ok
.setFocus()
870 self
.button_close
.setDefault(True)
871 self
.button_close
.setFocus()
873 qtutils
.connect_button(self
.button_ok
, self
.accept
)
874 qtutils
.connect_button(self
.button_close
, self
.reject
)
875 self
.init_state(None, self
.set_initial_size
)
877 def set_initial_size(self
):
878 width
= defs
.dialog_w
879 height
= defs
.msgbox_h
880 self
.resize(width
, height
)
882 def keyPressEvent(self
, event
):
883 """Handle Y/N hotkeys"""
886 QtCore
.QTimer
.singleShot(0, self
.accept
)
887 elif key
in (Qt
.Key_N
, Qt
.Key_Q
):
888 QtCore
.QTimer
.singleShot(0, self
.reject
)
889 elif key
== Qt
.Key_Tab
:
890 if self
.button_ok
.isVisible():
892 if self
.focusWidget() == self
.button_close
:
893 self
.button_ok
.setFocus()
895 self
.button_close
.setFocus()
897 Dialog
.keyPressEvent(self
, event
)
914 """Confirm that an action should take place"""
915 cancel_text
= cancel_text
or N_('Cancel')
916 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
919 parent
=qtutils
.active_window(),
922 info
=informative_text
,
925 cancel_text
=cancel_text
,
926 cancel_icon
=cancel_icon
,
931 return mbox
.run() == mbox
.Accepted
934 def critical(title
, message
=None, details
=None):
935 """Show a warning with the provided title and message."""
938 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxCritical
)
940 parent
=qtutils
.active_window(),
949 def command_error(title
, cmd
, status
, out
, err
):
950 """Report an error message about a failed command"""
951 details
= Interaction
.format_out_err(out
, err
)
952 message
= Interaction
.format_command_status(cmd
, status
)
953 critical(title
, message
=message
, details
=details
)
956 def information(title
, message
=None, details
=None, informative_text
=None):
957 """Show information with the provided title and message."""
961 parent
=qtutils
.active_window(),
964 info
=informative_text
,
971 def progress(title
, text
, parent
):
972 """Create a new ProgressDialog"""
973 return ProgressDialog(title
, text
, parent
)
976 def question(title
, text
, default
=True, logo
=None):
977 """Launches a QMessageBox question with the provided title and message.
978 Passing "default=False" will make "No" the default choice."""
979 parent
= qtutils
.active_window()
981 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
989 cancel_text
=N_('No'),
991 return msgbox
.run() == msgbox
.Accepted
994 def save_as(filename
, title
):
995 return qtutils
.save_as(filename
, title
=title
)
998 def async_command(title
, cmd
, runtask
):
999 task
= qtutils
.SimpleTask(partial(core
.run_command
, cmd
))
1000 task
.connect(partial(async_command_result
, title
, cmd
))
1004 def async_command_result(title
, cmd
, result
):
1005 status
, out
, err
= result
1006 cmd_string
= core
.list2cmdline(cmd
)
1007 Interaction
.command(title
, cmd_string
, status
, out
, err
)
1011 """Install the GUI-model interaction hooks"""
1012 Interaction
.critical
= staticmethod(critical
)
1013 Interaction
.confirm
= staticmethod(confirm
)
1014 Interaction
.question
= staticmethod(question
)
1015 Interaction
.information
= staticmethod(information
)
1016 Interaction
.command_error
= staticmethod(command_error
)
1017 Interaction
.save_as
= staticmethod(save_as
)
1018 Interaction
.async_command
= staticmethod(async_command
)