cmds: use version.check_git(...) in OpenParent
[git-cola.git] / cola / widgets / standard.py
blobb574e5677259a5501e6dc2e95bb4ff4a705a1278
1 from __future__ import division, absolute_import, unicode_literals
2 import time
3 from functools import partial
5 from qtpy import QtCore
6 from qtpy import QtGui
7 from qtpy import QtWidgets
8 from qtpy.QtCore import Qt
9 from qtpy.QtCore import Signal
10 from qtpy.QtWidgets import QDockWidget
12 from ..i18n import N_
13 from ..interaction import Interaction
14 from ..settings import Settings, mklist
15 from ..models import prefs
16 from .. import core
17 from .. import hotkeys
18 from .. import icons
19 from .. import qtcompat
20 from .. import qtutils
21 from .. import utils
22 from . import defs
25 class WidgetMixin(object):
26 """Mix-in for common utilities and serialization of widget state"""
28 def __init__(self):
29 self._unmaximized_rect = {}
31 def center(self):
32 parent = self.parent()
33 if parent is None:
34 return
35 left = parent.x()
36 width = parent.width()
37 center_x = left + width//2
38 x = center_x - self.width()//2
39 y = parent.y()
41 self.move(x, y)
43 def resize_to_desktop(self):
44 desktop = QtWidgets.QApplication.instance().desktop()
45 width = desktop.width()
46 height = desktop.height()
47 if utils.is_darwin():
48 self.resize(width, height)
49 else:
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.
54 self.show()
55 self.setWindowState(Qt.WindowMaximized)
56 if not shown:
57 self.hide()
59 def name(self):
60 """Returns the name of the view class"""
61 return self.__class__.__name__.lower()
63 def save_state(self, settings=None):
64 save = True
65 context = getattr(self, 'context', None)
66 if context:
67 cfg = context.cfg
68 save = cfg.get('cola.savewindowsettings', default=True)
69 if save:
70 if settings is None:
71 settings = Settings()
72 settings.load()
73 settings.save_gui_state(self)
75 def restore_state(self, settings=None):
76 if settings is None:
77 settings = Settings()
78 settings.load()
79 state = settings.get_gui_state(self)
80 if state:
81 result = self.apply_state(state)
82 else:
83 result = False
84 return result
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', '')
95 if 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)
102 self.move(x, y)
103 result = True
104 else:
105 result = False
106 return result
108 def export_state(self):
109 """Exports data for view save/restore"""
110 state = {}
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()
118 return state
120 def save_settings(self, settings=None):
121 return self.save_state(settings=settings)
123 def closeEvent(self, event):
124 self.save_settings()
125 self.Base.closeEvent(self, event)
127 def init_size(self, parent=None, settings=None, width=0, height=0):
128 if not width:
129 width = defs.dialog_w
130 if not height:
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)
138 self.center()
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):
148 def __init__(self):
149 WidgetMixin.__init__(self)
150 # Dockwidget options
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')
162 return state
164 def save_settings(self, settings=None):
165 if settings is None:
166 context = getattr(self, 'context', None)
167 settings = Settings()
168 settings.load()
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:
176 result = False
177 else:
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()
187 return result
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):
194 if self.lock_layout:
195 features = (QDockWidget.DockWidgetClosable |
196 QDockWidget.DockWidgetFloatable)
197 else:
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()
230 def move_up(self):
231 self.move(-1)
233 def move_down(self):
234 self.move(1)
236 def move(self, direction):
237 item = self.selected_item()
238 if item:
239 row = (self.row(item) + direction) % self.count()
240 elif self.count() > 0:
241 row = (self.count() + direction) % self.count()
242 else:
243 return
244 new_item = self.item(row)
245 if new_item:
246 self.setCurrentItem(new_item)
249 class TreeMixin(object):
251 def __init__(self, widget, Base):
252 self.widget = widget
253 self.Base = 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.
275 widget = self.widget
276 index = widget.currentIndex()
277 was_expanded = widget.isExpanded(index)
278 was_collapsed = not was_expanded
280 # Vim keybindings...
281 # Rewrite the event before marshalling to QTreeView.event()
282 key = event.key()
284 # Remap 'H' to 'Left'
285 if key == Qt.Key_H:
286 event = QtGui.QKeyEvent(event.type(),
287 Qt.Key_Left,
288 event.modifiers())
289 # Remap 'J' to 'Down'
290 elif key == Qt.Key_J:
291 event = QtGui.QKeyEvent(event.type(),
292 Qt.Key_Down,
293 event.modifiers())
294 # Remap 'K' to 'Up'
295 elif key == Qt.Key_K:
296 event = QtGui.QKeyEvent(event.type(),
297 Qt.Key_Up,
298 event.modifiers())
299 # Remap 'L' to 'Right'
300 elif key == Qt.Key_L:
301 event = QtGui.QKeyEvent(event.type(),
302 Qt.Key_Right,
303 event.modifiers())
305 # Re-read the event key to take the remappings into account
306 key = event.key()
307 if key == Qt.Key_Up:
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
314 # change focus.
315 widget.up.emit()
317 elif key == Qt.Key_Space:
318 widget.space.emit()
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
341 elif was_collapsed:
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())
350 if index.isValid():
351 widget.setCurrentIndex(index)
353 return result
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)
359 else:
360 index = self.model().itemFromIndex()
361 return index
363 def items(self):
364 root = self.widget.invisibleRootItem()
365 child = root.child
366 count = root.childCount()
367 return [child(i) for i in range(count)]
369 def selected_items(self):
370 """Return all selected items"""
371 widget = self.widget
372 if hasattr(widget, 'selectedItems'):
373 return widget.selectedItems()
374 else:
375 if hasattr(widget, 'itemFromIndex'):
376 item_from_index = widget.itemFromIndex
377 else:
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:
385 return None
386 return selected_items[0]
388 def current_item(self):
389 item = None
390 widget = self.widget
391 if hasattr(widget, 'currentItem'):
392 item = widget.currentItem()
393 else:
394 index = widget.currentIndex()
395 if index.isValid():
396 item = widget.model().itemFromIndex(index)
397 return item
399 def column_widths(self):
400 """Return the tree's column widths"""
401 widget = self.widget
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"""
407 if widths:
408 widget = self.widget
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"""
435 widget = self.widget
436 self.Base.dragEnterEvent(widget, event)
437 self._inner_drag = event.source() == widget
438 if self._inner_drag:
439 event.acceptProposedAction()
440 else:
441 event.ignore()
443 def dragLeaveEvent(self, event):
444 widget = self.widget
445 self.Base.dragLeaveEvent(widget, event)
446 if self._inner_drag:
447 event.accept()
448 else:
449 event.ignore()
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:
455 event.ignore()
456 return
457 widget = self.widget
458 clicked_items = self.selected_items()
459 event.setDropAction(Qt.MoveAction)
460 self.Base.dropEvent(widget, event)
462 if clicked_items:
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"""
472 widget = self.widget
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)
499 def accept(self):
500 self.save_settings()
501 return self.Base.accept(self)
503 def reject(self):
504 self.save_settings()
505 return self.Base.reject(self)
507 def close(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):
525 Mixin = TreeMixin
527 up = Signal()
528 space = Signal()
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()
547 def items(self):
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):
558 Mixin = TreeMixin
560 up = Signal()
561 space = Signal()
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()
580 def items(self):
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
611 prematurely closed.
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)
620 self.reset()
621 self.setRange(0, 0)
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)
642 def show(self):
643 QtWidgets.QApplication.setOverrideCursor(Qt.WaitCursor)
644 super(ProgressDialog, self).show()
645 self.thread.start()
647 def hide(self):
648 QtWidgets.QApplication.restoreOverrideCursor()
649 self.thread.stop()
650 self.thread.wait()
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)
662 self.running = False
663 self.txt = txt
664 self.timeout = timeout
665 self.symbols = [
666 '. ..',
667 '.. .',
668 '... ',
669 ' ... ',
670 ' ...',
672 self.idx = -1
674 def set_text(self, txt):
675 self.txt = txt
677 def cycle(self):
678 self.idx = (self.idx + 1) % len(self.symbols)
679 return self.txt + self.symbols[self.idx]
681 def stop(self):
682 self.running = False
684 def run(self):
685 self.running = True
686 while self.running:
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)
701 if step:
702 self.setSingleStep(step)
703 if value is not None:
704 self.setValue(value)
707 def export_header_columns(widget, state):
708 """Save QHeaderView column sizes"""
709 columns = []
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)
744 if parent:
745 self.setWindowModality(Qt.WindowModal)
746 if title:
747 self.setWindowTitle(title)
749 self.logo_label = QtWidgets.QLabel()
750 if logo:
751 # Render into a 1-inch wide pixmap
752 pixmap = logo.pixmap(defs.large_icon)
753 self.logo_label.setPixmap(pixmap)
754 else:
755 self.logo_label.hide()
757 self.text_label = QtWidgets.QLabel()
758 self.text_label.setText(text)
760 self.info_label = QtWidgets.QLabel()
761 if info:
762 self.info_label.setText(info)
763 else:
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)
775 if ok_text:
776 self.button_ok.setText(ok_text)
777 else:
778 self.button_ok.hide()
780 if default:
781 self.button_ok.setDefault(True)
782 self.button_ok.setFocus()
783 else:
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()
790 if details:
791 self.details_text.setFont(qtutils.default_monospace_font())
792 self.details_text.setPlainText(details)
793 else:
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,
810 self.top_layout,
811 self.buttons_layout,
812 self.details_text)
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))
832 else:
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"""
844 key = event.key()
845 if key == Qt.Key_Y:
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)
851 def run(self):
852 self.show()
853 return self.exec_()
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)
863 mbox = MessageBox(
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."""
874 if message is None:
875 message = title
876 logo = icons.from_style(QtWidgets.QStyle.SP_MessageBoxCritical)
877 mbox = MessageBox(
878 parent=qtutils.active_window(), title=title, text=message,
879 details=details, logo=logo)
880 mbox.run()
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."""
892 if message is None:
893 message = title
894 mbox = MessageBox(
895 parent=qtutils.active_window(), title=title, text=message,
896 info=informative_text, details=details, logo=icons.cola())
897 mbox.run()
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)
905 msgbox = MessageBox(
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))
919 runtask.start(task)
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)
928 def install():
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)