1 from __future__
import division
, absolute_import
, 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"""
29 self
._unmaximized
_rect
= {}
32 parent
= self
.parent()
36 width
= parent
.width()
37 center_x
= left
+ width
//2
38 x
= center_x
- self
.width()//2
43 def resize_to_desktop(self
):
44 desktop
= QtWidgets
.QApplication
.instance().desktop()
45 width
= desktop
.width()
46 height
= desktop
.height()
48 self
.resize(width
, height
)
50 shown
= self
.isVisible()
51 # earlier show() fools Windows focus stealing prevention. the main
52 # window is blocked for the duration of "git rebase" and we don't
53 # want to present a blocked window with git-xbase hidden somewhere.
55 self
.setWindowState(Qt
.WindowMaximized
)
60 """Returns the name of the view class"""
61 return self
.__class
__.__name
__.lower()
63 def save_state(self
, settings
=None):
65 context
= getattr(self
, 'context', None)
68 save
= cfg
.get('cola.savewindowsettings', default
=True)
73 settings
.save_gui_state(self
)
75 def restore_state(self
, settings
=None):
79 state
= settings
.get_gui_state(self
)
81 result
= self
.apply_state(state
)
86 def apply_state(self
, state
):
87 """Imports data for view save/restore"""
89 width
= utils
.asint(state
.get('width'))
90 height
= utils
.asint(state
.get('height'))
91 x
= utils
.asint(state
.get('x'))
92 y
= utils
.asint(state
.get('y'))
94 geometry
= state
.get('geometry', '')
96 from_base64
= QtCore
.QByteArray
.fromBase64
97 result
= self
.restoreGeometry(from_base64(core
.encode(geometry
)))
98 elif width
and height
:
99 # Users migrating from older versions won't have 'geometry'.
100 # They'll be upgraded to the new format on shutdown.
101 self
.resize(width
, height
)
108 def export_state(self
):
109 """Exports data for view save/restore"""
111 geometry
= self
.saveGeometry()
112 state
['geometry'] = geometry
.toBase64().data().decode('ascii')
113 # Until 2020: co-exist with older versions
114 state
['width'] = self
.width()
115 state
['height'] = self
.height()
116 state
['x'] = self
.x()
117 state
['y'] = self
.y()
120 def save_settings(self
, settings
=None):
121 return self
.save_state(settings
=settings
)
123 def closeEvent(self
, event
):
125 self
.Base
.closeEvent(self
, event
)
127 def init_size(self
, parent
=None, settings
=None, width
=0, height
=0):
129 width
= defs
.dialog_w
131 height
= defs
.dialog_h
132 self
.init_state(settings
, self
.resize_to_parent
, parent
, width
, height
)
134 def init_state(self
, settings
, callback
, *args
, **kwargs
):
135 """Restore saved settings or set the initial location"""
136 if not self
.restore_state(settings
=settings
):
137 callback(*args
, **kwargs
)
140 def resize_to_parent(self
, parent
, w
, h
):
141 """Set the initial size of the widget"""
142 width
, height
= qtutils
.default_size(parent
, w
, h
)
143 self
.resize(width
, height
)
146 class MainWindowMixin(WidgetMixin
):
149 WidgetMixin
.__init
__(self
)
151 self
.dockwidgets
= []
152 self
.lock_layout
= False
153 self
.widget_version
= 0
154 qtcompat
.set_common_dock_options(self
)
156 def export_state(self
):
157 """Exports data for save/restore"""
158 state
= WidgetMixin
.export_state(self
)
159 windowstate
= self
.saveState(self
.widget_version
)
160 state
['lock_layout'] = self
.lock_layout
161 state
['windowstate'] = windowstate
.toBase64().data().decode('ascii')
164 def save_settings(self
, settings
=None):
166 context
= getattr(self
, 'context', None)
167 settings
= Settings()
169 settings
.add_recent(core
.getcwd(), prefs
.maxrecent(context
))
170 return WidgetMixin
.save_settings(self
, settings
=settings
)
172 def apply_state(self
, state
):
173 result
= WidgetMixin
.apply_state(self
, state
)
174 windowstate
= state
.get('windowstate', None)
175 if windowstate
is None:
178 from_base64
= QtCore
.QByteArray
.fromBase64
179 result
= self
.restoreState(
180 from_base64(core
.encode(windowstate
)),
181 self
.widget_version
) and result
183 self
.lock_layout
= state
.get('lock_layout', self
.lock_layout
)
184 self
.update_dockwidget_lock_state()
185 self
.update_dockwidget_tooltips()
189 def set_lock_layout(self
, lock_layout
):
190 self
.lock_layout
= lock_layout
191 self
.update_dockwidget_lock_state()
193 def update_dockwidget_lock_state(self
):
195 features
= (QDockWidget
.DockWidgetClosable |
196 QDockWidget
.DockWidgetFloatable
)
198 features
= (QDockWidget
.DockWidgetClosable |
199 QDockWidget
.DockWidgetFloatable |
200 QDockWidget
.DockWidgetMovable
)
201 for widget
in self
.dockwidgets
:
202 widget
.titleBarWidget().update_tooltips()
203 widget
.setFeatures(features
)
205 def update_dockwidget_tooltips(self
):
206 for widget
in self
.dockwidgets
:
207 widget
.titleBarWidget().update_tooltips()
210 class ListWidget(QtWidgets
.QListWidget
):
211 """QListWidget with vim j/k navigation hotkeys"""
213 def __init__(self
, parent
=None):
214 super(ListWidget
, self
).__init
__(parent
)
216 self
.up_action
= qtutils
.add_action(
217 self
, N_('Move Up'), self
.move_up
,
218 hotkeys
.MOVE_UP
, hotkeys
.MOVE_UP_SECONDARY
)
220 self
.down_action
= qtutils
.add_action(
221 self
, N_('Move Down'), self
.move_down
,
222 hotkeys
.MOVE_DOWN
, hotkeys
.MOVE_DOWN_SECONDARY
)
224 def selected_item(self
):
225 return self
.currentItem()
227 def selected_items(self
):
228 return self
.selectedItems()
236 def move(self
, direction
):
237 item
= self
.selected_item()
239 row
= (self
.row(item
) + direction
) % self
.count()
240 elif self
.count() > 0:
241 row
= (self
.count() + direction
) % self
.count()
244 new_item
= self
.item(row
)
246 self
.setCurrentItem(new_item
)
249 class TreeMixin(object):
251 def __init__(self
, widget
, Base
):
255 widget
.setAlternatingRowColors(True)
256 widget
.setUniformRowHeights(True)
257 widget
.setAllColumnsShowFocus(True)
258 widget
.setAnimated(True)
259 widget
.setRootIsDecorated(False)
261 def keyPressEvent(self
, event
):
263 Make LeftArrow to work on non-directories.
265 When LeftArrow is pressed on a file entry or an unexpanded
266 directory, then move the current index to the parent directory.
268 This simplifies navigation using the keyboard.
269 For power-users, we support Vim keybindings ;-P
272 # Check whether the item is expanded before calling the base class
273 # keyPressEvent otherwise we end up collapsing and changing the
274 # current index in one shot, which we don't want to do.
276 index
= widget
.currentIndex()
277 was_expanded
= widget
.isExpanded(index
)
278 was_collapsed
= not was_expanded
281 # Rewrite the event before marshalling to QTreeView.event()
284 # Remap 'H' to 'Left'
286 event
= QtGui
.QKeyEvent(event
.type(),
289 # Remap 'J' to 'Down'
290 elif key
== Qt
.Key_J
:
291 event
= QtGui
.QKeyEvent(event
.type(),
295 elif key
== Qt
.Key_K
:
296 event
= QtGui
.QKeyEvent(event
.type(),
299 # Remap 'L' to 'Right'
300 elif key
== Qt
.Key_L
:
301 event
= QtGui
.QKeyEvent(event
.type(),
305 # Re-read the event key to take the remappings into account
308 idxs
= widget
.selectedIndexes()
309 rows
= [idx
.row() for idx
in idxs
]
310 if len(rows
) == 1 and rows
[0] == 0:
311 # The cursor is at the beginning of the line.
312 # If we have selection then simply reset the cursor.
313 # Otherwise, emit a signal so that the parent can
317 elif key
== Qt
.Key_Space
:
320 result
= self
.Base
.keyPressEvent(widget
, event
)
322 # Let others hook in here before we change the indexes
323 widget
.index_about_to_change
.emit()
325 # Automatically select the first entry when expanding a directory
326 if (key
== Qt
.Key_Right
and was_collapsed
and
327 widget
.isExpanded(index
)):
328 index
= widget
.moveCursor(widget
.MoveDown
, event
.modifiers())
329 widget
.setCurrentIndex(index
)
331 # Process non-root entries with valid parents only.
332 elif key
== Qt
.Key_Left
and index
.parent().isValid():
334 # File entries have rowCount() == 0
335 model
= widget
.model()
336 if (hasattr(model
, 'itemFromIndex')
337 and model
.itemFromIndex(index
).rowCount() == 0):
338 widget
.setCurrentIndex(index
.parent())
340 # Otherwise, do this for collapsed directories only
342 widget
.setCurrentIndex(index
.parent())
344 # If it's a movement key ensure we have a selection
345 elif key
in (Qt
.Key_Left
, Qt
.Key_Up
, Qt
.Key_Right
, Qt
.Key_Down
):
346 # Try to select the first item if the model index is invalid
347 item
= self
.selected_item()
348 if item
is None or not index
.isValid():
349 index
= widget
.model().index(0, 0, QtCore
.QModelIndex())
351 widget
.setCurrentIndex(index
)
355 def item_from_index(self
, item
):
356 """Return a QModelIndex from the provided item"""
357 if hasattr(self
, 'itemFromIndex'):
358 index
= self
.itemFromIndex(item
)
360 index
= self
.model().itemFromIndex()
364 root
= self
.widget
.invisibleRootItem()
366 count
= root
.childCount()
367 return [child(i
) for i
in range(count
)]
369 def selected_items(self
):
370 """Return all selected items"""
372 if hasattr(widget
, 'selectedItems'):
373 return widget
.selectedItems()
375 if hasattr(widget
, 'itemFromIndex'):
376 item_from_index
= widget
.itemFromIndex
378 item_from_index
= widget
.model().itemFromIndex
379 return [item_from_index(i
) for i
in widget
.selectedIndexes()]
381 def selected_item(self
):
382 """Return the first selected item"""
383 selected_items
= self
.selected_items()
384 if not selected_items
:
386 return selected_items
[0]
388 def current_item(self
):
391 if hasattr(widget
, 'currentItem'):
392 item
= widget
.currentItem()
394 index
= widget
.currentIndex()
396 item
= widget
.model().itemFromIndex(index
)
399 def column_widths(self
):
400 """Return the tree's column widths"""
402 count
= widget
.header().count()
403 return [widget
.columnWidth(i
) for i
in range(count
)]
405 def set_column_widths(self
, widths
):
406 """Set the tree's column widths"""
409 count
= widget
.header().count()
410 if len(widths
) > count
:
411 widths
= widths
[:count
]
412 for idx
, value
in enumerate(widths
):
413 widget
.setColumnWidth(idx
, value
)
416 class DraggableTreeMixin(TreeMixin
):
417 """A tree widget with internal drag+drop reordering of rows
419 Expects that the widget provides an `items_moved` signal.
422 def __init__(self
, widget
, Base
):
423 super(DraggableTreeMixin
, self
).__init
__(widget
, Base
)
425 self
._inner
_drag
= False
426 widget
.setAcceptDrops(True)
427 widget
.setSelectionMode(widget
.SingleSelection
)
428 widget
.setDragEnabled(True)
429 widget
.setDropIndicatorShown(True)
430 widget
.setDragDropMode(QtWidgets
.QAbstractItemView
.InternalMove
)
431 widget
.setSortingEnabled(False)
433 def dragEnterEvent(self
, event
):
434 """Accept internal drags only"""
436 self
.Base
.dragEnterEvent(widget
, event
)
437 self
._inner
_drag
= event
.source() == widget
439 event
.acceptProposedAction()
443 def dragLeaveEvent(self
, event
):
445 self
.Base
.dragLeaveEvent(widget
, event
)
450 self
._inner
_drag
= False
452 def dropEvent(self
, event
):
453 """Re-select selected items after an internal move"""
454 if not self
._inner
_drag
:
458 clicked_items
= self
.selected_items()
459 event
.setDropAction(Qt
.MoveAction
)
460 self
.Base
.dropEvent(widget
, event
)
463 widget
.clearSelection()
464 for item
in clicked_items
:
465 item
.setSelected(True)
466 widget
.items_moved
.emit(clicked_items
)
467 self
._inner
_drag
= False
468 event
.accept() # must be called after dropEvent()
470 def mousePressEvent(self
, event
):
471 """Clear the selection when a mouse click hits no item"""
473 clicked_item
= widget
.itemAt(event
.pos())
474 if clicked_item
is None:
475 widget
.clearSelection()
476 return self
.Base
.mousePressEvent(widget
, event
)
479 class Widget(WidgetMixin
, QtWidgets
.QWidget
):
480 Base
= QtWidgets
.QWidget
482 def __init__(self
, parent
=None):
483 QtWidgets
.QWidget
.__init
__(self
, parent
)
484 WidgetMixin
.__init
__(self
)
487 class Dialog(WidgetMixin
, QtWidgets
.QDialog
):
488 Base
= QtWidgets
.QDialog
490 def __init__(self
, parent
=None):
491 QtWidgets
.QDialog
.__init
__(self
, parent
)
492 WidgetMixin
.__init
__(self
)
493 # Disable the Help button hint on Windows
494 if hasattr(Qt
, 'WindowContextHelpButtonHint'):
495 help_hint
= Qt
.WindowContextHelpButtonHint
496 flags
= self
.windowFlags() & ~help_hint
497 self
.setWindowFlags(flags
)
501 return self
.Base
.accept(self
)
505 return self
.Base
.reject(self
)
508 """save_settings() is handled by accept() and reject()"""
509 self
.Base
.close(self
)
511 def closeEvent(self
, event
):
512 """save_settings() is handled by accept() and reject()"""
513 self
.Base
.closeEvent(self
, event
)
516 class MainWindow(MainWindowMixin
, QtWidgets
.QMainWindow
):
517 Base
= QtWidgets
.QMainWindow
519 def __init__(self
, parent
=None):
520 QtWidgets
.QMainWindow
.__init
__(self
, parent
)
521 MainWindowMixin
.__init
__(self
)
524 class TreeView(QtWidgets
.QTreeView
):
529 index_about_to_change
= Signal()
531 def __init__(self
, parent
=None):
532 QtWidgets
.QTreeView
.__init
__(self
, parent
)
533 self
._mixin
= self
.Mixin(self
, QtWidgets
.QTreeView
)
535 def keyPressEvent(self
, event
):
536 return self
._mixin
.keyPressEvent(event
)
538 def current_item(self
):
539 return self
._mixin
.current_item()
541 def selected_item(self
):
542 return self
._mixin
.selected_item()
544 def selected_items(self
):
545 return self
._mixin
.selected_items()
548 return self
._mixin
.items()
550 def column_widths(self
):
551 return self
._mixin
.column_widths()
553 def set_column_widths(self
, widths
):
554 return self
._mixin
.set_column_widths(widths
)
557 class TreeWidget(QtWidgets
.QTreeWidget
):
562 index_about_to_change
= Signal()
564 def __init__(self
, parent
=None):
565 super(TreeWidget
, self
).__init
__(parent
)
566 self
._mixin
= self
.Mixin(self
, QtWidgets
.QTreeWidget
)
568 def keyPressEvent(self
, event
):
569 return self
._mixin
.keyPressEvent(event
)
571 def current_item(self
):
572 return self
._mixin
.current_item()
574 def selected_item(self
):
575 return self
._mixin
.selected_item()
577 def selected_items(self
):
578 return self
._mixin
.selected_items()
581 return self
._mixin
.items()
583 def column_widths(self
):
584 return self
._mixin
.column_widths()
586 def set_column_widths(self
, widths
):
587 return self
._mixin
.set_column_widths(widths
)
590 class DraggableTreeWidget(TreeWidget
):
591 Mixin
= DraggableTreeMixin
592 items_moved
= Signal(object)
594 def mousePressEvent(self
, event
):
595 return self
._mixin
.mousePressEvent(event
)
597 def dropEvent(self
, event
):
598 return self
._mixin
.dropEvent(event
)
600 def dragLeaveEvent(self
, event
):
601 return self
._mixin
.dragLeaveEvent(event
)
603 def dragEnterEvent(self
, event
):
604 return self
._mixin
.dragEnterEvent(event
)
607 class ProgressDialog(QtWidgets
.QProgressDialog
):
608 """Custom progress dialog
610 This dialog ignores the ESC key so that it is not
613 A thread is spawned to animate the progress label text.
616 def __init__(self
, title
, label
, parent
):
617 QtWidgets
.QProgressDialog
.__init
__(self
, parent
)
618 if parent
is not None:
619 self
.setWindowModality(Qt
.WindowModal
)
622 self
.setMinimumDuration(0)
623 self
.setCancelButton(None)
624 self
.setFont(qtutils
.default_monospace_font())
625 self
.thread
= ProgressAnimationThread(label
, self
)
626 self
.thread
.updated
.connect(self
.refresh
, type=Qt
.QueuedConnection
)
628 self
.set_details(title
, label
)
630 def set_details(self
, title
, label
):
631 self
.setWindowTitle(title
)
632 self
.setLabelText(label
+ ' ')
633 self
.thread
.set_text(label
)
635 def refresh(self
, txt
):
636 self
.setLabelText(txt
)
638 def keyPressEvent(self
, event
):
639 if event
.key() != Qt
.Key_Escape
:
640 super(ProgressDialog
, self
).keyPressEvent(event
)
643 QtWidgets
.QApplication
.setOverrideCursor(Qt
.WaitCursor
)
644 super(ProgressDialog
, self
).show()
648 QtWidgets
.QApplication
.restoreOverrideCursor()
651 super(ProgressDialog
, self
).hide()
654 class ProgressAnimationThread(QtCore
.QThread
):
655 """Emits a pseudo-animated text stream for progress bars
658 updated
= Signal(object)
660 def __init__(self
, txt
, parent
, timeout
=0.1):
661 QtCore
.QThread
.__init
__(self
, parent
)
664 self
.timeout
= timeout
674 def set_text(self
, txt
):
678 self
.idx
= (self
.idx
+ 1) % len(self
.symbols
)
679 return self
.txt
+ self
.symbols
[self
.idx
]
687 self
.updated
.emit(self
.cycle())
688 time
.sleep(self
.timeout
)
691 class SpinBox(QtWidgets
.QSpinBox
):
693 def __init__(self
, parent
=None, value
=None,
694 mini
=1, maxi
=99999, step
=0, prefix
='', suffix
=''):
695 QtWidgets
.QSpinBox
.__init
__(self
, parent
)
696 self
.setPrefix(prefix
)
697 self
.setSuffix(suffix
)
698 self
.setWrapping(True)
699 self
.setMinimum(mini
)
700 self
.setMaximum(maxi
)
702 self
.setSingleStep(step
)
703 if value
is not None:
707 def export_header_columns(widget
, state
):
708 """Save QHeaderView column sizes"""
710 header
= widget
.horizontalHeader()
711 for idx
in range(header
.count()):
712 columns
.append(header
.sectionSize(idx
))
714 state
['columns'] = columns
717 def apply_header_columns(widget
, state
):
718 """Apply QHeaderView column sizes"""
719 columns
= mklist(state
.get('columns', []))
720 header
= widget
.horizontalHeader()
721 if header
.stretchLastSection():
722 # Setting the size will make the section wider than necessary, which
723 # defeats the purpose of the stretch flag. Skip the last column when
724 # it's stretchy so that it retains the stretchy behavior.
725 columns
= columns
[:-1]
726 for idx
, size
in enumerate(columns
):
727 header
.resizeSection(idx
, size
)
730 class MessageBox(Dialog
):
731 """Improved QMessageBox replacement
733 QMessageBox has a lot of usability issues. It sometimes cannot be
734 resized, and it brings along a lots of annoying properties that we'd have
735 to workaround, so we use a simple custom dialog instead.
738 def __init__(self
, parent
=None, title
='', text
='',
739 info
='', details
='', logo
=None, default
=False,
740 ok_icon
=None, ok_text
='', cancel_text
=None, cancel_icon
=None):
742 Dialog
.__init
__(self
, parent
=parent
)
745 self
.setWindowModality(Qt
.WindowModal
)
747 self
.setWindowTitle(title
)
749 self
.logo_label
= QtWidgets
.QLabel()
751 # Render into a 1-inch wide pixmap
752 pixmap
= logo
.pixmap(defs
.large_icon
)
753 self
.logo_label
.setPixmap(pixmap
)
755 self
.logo_label
.hide()
757 self
.text_label
= QtWidgets
.QLabel()
758 self
.text_label
.setText(text
)
760 self
.info_label
= QtWidgets
.QLabel()
762 self
.info_label
.setText(info
)
764 self
.info_label
.hide()
766 ok_icon
= icons
.mkicon(ok_icon
, icons
.ok
)
767 self
.button_ok
= qtutils
.create_button(text
=ok_text
, icon
=ok_icon
)
769 self
.button_toggle_details
= qtutils
.create_button(
770 text
=N_('Show Details...'))
772 self
.button_close
= qtutils
.close_button(
773 text
=cancel_text
, icon
=cancel_icon
)
776 self
.button_ok
.setText(ok_text
)
778 self
.button_ok
.hide()
781 self
.button_ok
.setDefault(True)
782 self
.button_ok
.setFocus()
784 self
.button_close
.setDefault(True)
785 self
.button_close
.setFocus()
787 self
.details_text
= QtWidgets
.QPlainTextEdit()
788 self
.details_text
.setReadOnly(True)
789 self
.details_text
.hide()
791 self
.details_text
.setFont(qtutils
.default_monospace_font())
792 self
.details_text
.setPlainText(details
)
794 self
.button_toggle_details
.hide()
796 self
.info_layout
= qtutils
.vbox(
797 defs
.large_margin
, defs
.button_spacing
,
798 self
.text_label
, self
.info_label
, qtutils
.STRETCH
)
800 self
.top_layout
= qtutils
.hbox(
801 defs
.large_margin
, defs
.button_spacing
,
802 self
.logo_label
, self
.info_layout
, qtutils
.STRETCH
)
804 self
.buttons_layout
= qtutils
.hbox(
805 defs
.no_margin
, defs
.button_spacing
, qtutils
.STRETCH
,
806 self
.button_toggle_details
, self
.button_close
, self
.button_ok
)
808 self
.main_layout
= qtutils
.vbox(
809 defs
.margin
, defs
.button_spacing
,
813 self
.main_layout
.setStretchFactor(self
.details_text
, 2)
814 self
.setLayout(self
.main_layout
)
816 qtutils
.connect_button(self
.button_ok
, self
.accept
)
817 qtutils
.connect_button(self
.button_close
, self
.reject
)
818 qtutils
.connect_button(self
.button_toggle_details
, self
.toggle_details
)
819 self
.init_state(None, self
.set_initial_size
)
821 def set_initial_size(self
):
822 width
= defs
.dialog_w
823 height
= defs
.msgbox_h
824 self
.resize(width
, height
)
826 def toggle_details(self
):
827 if self
.details_text
.isVisible():
828 text
= N_('Show Details...')
829 self
.details_text
.hide()
830 QtCore
.QTimer
.singleShot(
831 0, lambda: self
.resize(self
.width(), defs
.msgbox_h
))
833 text
= N_('Hide Details..')
834 self
.details_text
.show()
835 new_height
= defs
.msgbox_h
* 4
836 if self
.height() < new_height
:
837 QtCore
.QTimer
.singleShot(
838 0, lambda: self
.resize(self
.width(), new_height
))
840 self
.button_toggle_details
.setText(text
)
842 def keyPressEvent(self
, event
):
843 """Handle Y/N hotkeys"""
846 QtCore
.QTimer
.singleShot(0, self
.accept
)
847 elif key
in (Qt
.Key_N
, Qt
.Key_Q
):
848 QtCore
.QTimer
.singleShot(0, self
.reject
)
849 return Dialog
.keyPressEvent(self
, event
)
856 def confirm(title
, text
, informative_text
, ok_text
,
857 icon
=None, default
=True,
858 cancel_text
=None, cancel_icon
=None):
859 """Confirm that an action should take place"""
860 cancel_text
= cancel_text
or N_('Cancel')
861 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
864 parent
=qtutils
.active_window(), title
=title
, text
=text
,
865 info
=informative_text
, ok_text
=ok_text
, ok_icon
=icon
,
866 cancel_text
=cancel_text
, cancel_icon
=cancel_icon
,
867 logo
=logo
, default
=default
)
869 return mbox
.run() == mbox
.Accepted
872 def critical(title
, message
=None, details
=None):
873 """Show a warning with the provided title and message."""
876 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxCritical
)
878 parent
=qtutils
.active_window(), title
=title
, text
=message
,
879 details
=details
, logo
=logo
)
883 def command_error(title
, cmd
, status
, out
, err
):
884 """Report an error message about a failed command"""
885 details
= Interaction
.format_out_err(out
, err
)
886 message
= Interaction
.format_command_status(cmd
, status
)
887 critical(title
, message
=message
, details
=details
)
890 def information(title
, message
=None, details
=None, informative_text
=None):
891 """Show information with the provided title and message."""
895 parent
=qtutils
.active_window(), title
=title
, text
=message
,
896 info
=informative_text
, details
=details
, logo
=icons
.cola())
900 def question(title
, text
, default
=True):
901 """Launches a QMessageBox question with the provided title and message.
902 Passing "default=False" will make "No" the default choice."""
903 parent
= qtutils
.active_window()
904 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
906 parent
=parent
, title
=title
, text
=text
, default
=default
, logo
=logo
,
907 ok_text
=N_('Yes'), cancel_text
=N_('No'))
908 return msgbox
.run() == msgbox
.Accepted
911 def save_as(filename
, title
):
912 return qtutils
.save_as(filename
, title
=title
)
915 def async_command(title
, cmd
, runtask
):
916 parent
= qtutils
.active_window()
917 task
= qtutils
.SimpleTask(parent
, partial(core
.run_command
, cmd
))
918 task
.connect(partial(async_command_result
, title
, cmd
))
922 def async_command_result(title
, cmd
, result
):
923 status
, out
, err
= result
924 cmd_string
= core
.list2cmdline(cmd
)
925 Interaction
.command(title
, cmd_string
, status
, out
, err
)
929 """Install the GUI-model interaction hooks"""
930 Interaction
.critical
= staticmethod(critical
)
931 Interaction
.confirm
= staticmethod(confirm
)
932 Interaction
.question
= staticmethod(question
)
933 Interaction
.information
= staticmethod(information
)
934 Interaction
.command_error
= staticmethod(command_error
)
935 Interaction
.save_as
= staticmethod(save_as
)
936 Interaction
.async_command
= staticmethod(async_command
)