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', '')
176 from_base64
= QtCore
.QByteArray
.fromBase64
177 result
= self
.restoreState(
178 from_base64(core
.encode(windowstate
)),
179 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 metrics
= QtGui
.QFontMetrics(font
)
708 width
= max(self
.minimumWidth(), metrics
.width('XXXXXX'))
709 self
.setMinimumWidth(width
)
712 def export_header_columns(widget
, state
):
713 """Save QHeaderView column sizes"""
715 header
= widget
.horizontalHeader()
716 for idx
in range(header
.count()):
717 columns
.append(header
.sectionSize(idx
))
719 state
['columns'] = columns
722 def apply_header_columns(widget
, state
):
723 """Apply QHeaderView column sizes"""
724 columns
= mklist(state
.get('columns', []))
725 header
= widget
.horizontalHeader()
726 if header
.stretchLastSection():
727 # Setting the size will make the section wider than necessary, which
728 # defeats the purpose of the stretch flag. Skip the last column when
729 # it's stretchy so that it retains the stretchy behavior.
730 columns
= columns
[:-1]
731 for idx
, size
in enumerate(columns
):
732 header
.resizeSection(idx
, size
)
735 class MessageBox(Dialog
):
736 """Improved QMessageBox replacement
738 QMessageBox has a lot of usability issues. It sometimes cannot be
739 resized, and it brings along a lots of annoying properties that we'd have
740 to workaround, so we use a simple custom dialog instead.
743 def __init__(self
, parent
=None, title
='', text
='',
744 info
='', details
='', logo
=None, default
=False,
745 ok_icon
=None, ok_text
='', cancel_text
=None, cancel_icon
=None):
747 Dialog
.__init
__(self
, parent
=parent
)
750 self
.setWindowModality(Qt
.WindowModal
)
752 self
.setWindowTitle(title
)
754 self
.logo_label
= QtWidgets
.QLabel()
756 # Render into a 1-inch wide pixmap
757 pixmap
= logo
.pixmap(defs
.large_icon
)
758 self
.logo_label
.setPixmap(pixmap
)
760 self
.logo_label
.hide()
762 self
.text_label
= QtWidgets
.QLabel()
763 self
.text_label
.setText(text
)
765 self
.info_label
= QtWidgets
.QLabel()
767 self
.info_label
.setText(info
)
769 self
.info_label
.hide()
771 ok_icon
= icons
.mkicon(ok_icon
, icons
.ok
)
772 self
.button_ok
= qtutils
.create_button(text
=ok_text
, icon
=ok_icon
)
774 self
.button_toggle_details
= qtutils
.create_button(
775 text
=N_('Show Details...'))
777 self
.button_close
= qtutils
.close_button(
778 text
=cancel_text
, icon
=cancel_icon
)
781 self
.button_ok
.setText(ok_text
)
783 self
.button_ok
.hide()
786 self
.button_ok
.setDefault(True)
787 self
.button_ok
.setFocus()
789 self
.button_close
.setDefault(True)
790 self
.button_close
.setFocus()
792 self
.details_text
= QtWidgets
.QPlainTextEdit()
793 self
.details_text
.setReadOnly(True)
794 self
.details_text
.hide()
796 self
.details_text
.setFont(qtutils
.default_monospace_font())
797 self
.details_text
.setPlainText(details
)
799 self
.button_toggle_details
.hide()
801 self
.info_layout
= qtutils
.vbox(
802 defs
.large_margin
, defs
.button_spacing
,
803 self
.text_label
, self
.info_label
, qtutils
.STRETCH
)
805 self
.top_layout
= qtutils
.hbox(
806 defs
.large_margin
, defs
.button_spacing
,
807 self
.logo_label
, self
.info_layout
, qtutils
.STRETCH
)
809 self
.buttons_layout
= qtutils
.hbox(
810 defs
.no_margin
, defs
.button_spacing
, qtutils
.STRETCH
,
811 self
.button_toggle_details
, self
.button_close
, self
.button_ok
)
813 self
.main_layout
= qtutils
.vbox(
814 defs
.margin
, defs
.button_spacing
,
818 self
.main_layout
.setStretchFactor(self
.details_text
, 2)
819 self
.setLayout(self
.main_layout
)
821 qtutils
.connect_button(self
.button_ok
, self
.accept
)
822 qtutils
.connect_button(self
.button_close
, self
.reject
)
823 qtutils
.connect_button(self
.button_toggle_details
, self
.toggle_details
)
824 self
.init_state(None, self
.set_initial_size
)
826 def set_initial_size(self
):
827 width
= defs
.dialog_w
828 height
= defs
.msgbox_h
829 self
.resize(width
, height
)
831 def toggle_details(self
):
832 if self
.details_text
.isVisible():
833 text
= N_('Show Details...')
834 self
.details_text
.hide()
835 QtCore
.QTimer
.singleShot(
836 0, lambda: self
.resize(self
.width(), defs
.msgbox_h
))
838 text
= N_('Hide Details..')
839 self
.details_text
.show()
840 new_height
= defs
.msgbox_h
* 4
841 if self
.height() < new_height
:
842 QtCore
.QTimer
.singleShot(
843 0, lambda: self
.resize(self
.width(), new_height
))
845 self
.button_toggle_details
.setText(text
)
847 def keyPressEvent(self
, event
):
848 """Handle Y/N hotkeys"""
851 QtCore
.QTimer
.singleShot(0, self
.accept
)
852 elif key
in (Qt
.Key_N
, Qt
.Key_Q
):
853 QtCore
.QTimer
.singleShot(0, self
.reject
)
854 return Dialog
.keyPressEvent(self
, event
)
861 def confirm(title
, text
, informative_text
, ok_text
,
862 icon
=None, default
=True,
863 cancel_text
=None, cancel_icon
=None):
864 """Confirm that an action should take place"""
865 cancel_text
= cancel_text
or N_('Cancel')
866 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
869 parent
=qtutils
.active_window(), title
=title
, text
=text
,
870 info
=informative_text
, ok_text
=ok_text
, ok_icon
=icon
,
871 cancel_text
=cancel_text
, cancel_icon
=cancel_icon
,
872 logo
=logo
, default
=default
)
874 return mbox
.run() == mbox
.Accepted
877 def critical(title
, message
=None, details
=None):
878 """Show a warning with the provided title and message."""
881 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxCritical
)
883 parent
=qtutils
.active_window(), title
=title
, text
=message
,
884 details
=details
, logo
=logo
)
888 def command_error(title
, cmd
, status
, out
, err
):
889 """Report an error message about a failed command"""
890 details
= Interaction
.format_out_err(out
, err
)
891 message
= Interaction
.format_command_status(cmd
, status
)
892 critical(title
, message
=message
, details
=details
)
895 def information(title
, message
=None, details
=None, informative_text
=None):
896 """Show information with the provided title and message."""
900 parent
=qtutils
.active_window(), title
=title
, text
=message
,
901 info
=informative_text
, details
=details
, logo
=icons
.cola())
905 def question(title
, text
, default
=True):
906 """Launches a QMessageBox question with the provided title and message.
907 Passing "default=False" will make "No" the default choice."""
908 parent
= qtutils
.active_window()
909 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
911 parent
=parent
, title
=title
, text
=text
, default
=default
, logo
=logo
,
912 ok_text
=N_('Yes'), cancel_text
=N_('No'))
913 return msgbox
.run() == msgbox
.Accepted
916 def save_as(filename
, title
):
917 return qtutils
.save_as(filename
, title
=title
)
920 def async_command(title
, cmd
, runtask
):
921 parent
= qtutils
.active_window()
922 task
= qtutils
.SimpleTask(parent
, partial(core
.run_command
, cmd
))
923 task
.connect(partial(async_command_result
, title
, cmd
))
927 def async_command_result(title
, cmd
, result
):
928 status
, out
, err
= result
929 cmd_string
= core
.list2cmdline(cmd
)
930 Interaction
.command(title
, cmd_string
, status
, out
, err
)
934 """Install the GUI-model interaction hooks"""
935 Interaction
.critical
= staticmethod(critical
)
936 Interaction
.confirm
= staticmethod(confirm
)
937 Interaction
.question
= staticmethod(question
)
938 Interaction
.information
= staticmethod(information
)
939 Interaction
.command_error
= staticmethod(command_error
)
940 Interaction
.save_as
= staticmethod(save_as
)
941 Interaction
.async_command
= staticmethod(async_command
)