1 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
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 width
, height
= qtutils
.desktop_size()
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-cola-sequence-editor
56 self
.setWindowState(Qt
.WindowMaximized
)
61 """Returns the name of the view class"""
62 return self
.__class
__.__name
__.lower()
64 def save_state(self
, settings
=None):
66 context
= getattr(self
, 'context', None)
69 save
= cfg
.get('cola.savewindowsettings', default
=True)
72 settings
= Settings
.read()
73 settings
.save_gui_state(self
)
75 def restore_state(self
, settings
=None):
77 settings
= Settings
.read()
78 state
= settings
.get_gui_state(self
)
80 result
= self
.apply_state(state
)
85 def apply_state(self
, state
):
86 """Imports data for view save/restore"""
88 width
= utils
.asint(state
.get('width'))
89 height
= utils
.asint(state
.get('height'))
90 x
= utils
.asint(state
.get('x'))
91 y
= utils
.asint(state
.get('y'))
93 geometry
= state
.get('geometry', '')
95 from_base64
= QtCore
.QByteArray
.fromBase64
96 result
= self
.restoreGeometry(from_base64(core
.encode(geometry
)))
97 elif width
and height
:
98 # Users migrating from older versions won't have 'geometry'.
99 # They'll be upgraded to the new format on shutdown.
100 self
.resize(width
, height
)
107 def export_state(self
):
108 """Exports data for view save/restore"""
110 geometry
= self
.saveGeometry()
111 state
['geometry'] = geometry
.toBase64().data().decode('ascii')
112 # Until 2020: co-exist with older versions
113 state
['width'] = self
.width()
114 state
['height'] = self
.height()
115 state
['x'] = self
.x()
116 state
['y'] = self
.y()
119 def save_settings(self
, settings
=None):
120 return self
.save_state(settings
=settings
)
122 def closeEvent(self
, event
):
124 self
.closed
.emit(self
)
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
):
148 WidgetMixin
.__init
__(self
)
150 self
.dockwidgets
= []
151 self
.lock_layout
= False
152 self
.widget_version
= 0
153 qtcompat
.set_common_dock_options(self
)
154 self
.default_state
= None
156 def init_state(self
, settings
, callback
, *args
, **kwargs
):
157 """Save the initial state before calling the parent initializer"""
158 self
.default_state
= self
.saveState(self
.widget_version
)
159 super().init_state(settings
, callback
, *args
, **kwargs
)
161 def export_state(self
):
162 """Exports data for save/restore"""
163 state
= WidgetMixin
.export_state(self
)
164 windowstate
= self
.saveState(self
.widget_version
)
165 state
['lock_layout'] = self
.lock_layout
166 state
['windowstate'] = windowstate
.toBase64().data().decode('ascii')
169 def save_settings(self
, settings
=None):
171 context
= getattr(self
, 'context', None)
173 settings
= Settings
.read()
175 settings
= context
.settings
177 settings
.add_recent(core
.getcwd(), prefs
.maxrecent(context
))
178 return WidgetMixin
.save_settings(self
, settings
=settings
)
180 def apply_state(self
, state
):
181 result
= WidgetMixin
.apply_state(self
, state
)
182 windowstate
= state
.get('windowstate', '')
184 from_base64
= QtCore
.QByteArray
.fromBase64
187 from_base64(core
.encode(windowstate
)), self
.widget_version
194 self
.lock_layout
= state
.get('lock_layout', self
.lock_layout
)
195 self
.update_dockwidget_lock_state()
196 self
.update_dockwidget_tooltips()
200 def reset_layout(self
):
201 self
.restoreState(self
.default_state
, self
.widget_version
)
203 def set_lock_layout(self
, lock_layout
):
204 self
.lock_layout
= lock_layout
205 self
.update_dockwidget_lock_state()
207 def update_dockwidget_lock_state(self
):
209 features
= QDockWidget
.DockWidgetClosable
212 QDockWidget
.DockWidgetClosable
213 | QDockWidget
.DockWidgetFloatable
214 | QDockWidget
.DockWidgetMovable
216 for widget
in self
.dockwidgets
:
217 widget
.titleBarWidget().update_tooltips()
218 widget
.setFeatures(features
)
220 def update_dockwidget_tooltips(self
):
221 for widget
in self
.dockwidgets
:
222 widget
.titleBarWidget().update_tooltips()
225 class ListWidget(QtWidgets
.QListWidget
):
226 """QListWidget with vim j/k navigation hotkeys"""
228 def __init__(self
, parent
=None):
229 super().__init
__(parent
)
231 self
.up_action
= qtutils
.add_action(
236 hotkeys
.MOVE_UP_SECONDARY
,
239 self
.down_action
= qtutils
.add_action(
244 hotkeys
.MOVE_DOWN_SECONDARY
,
247 def selected_item(self
):
248 return self
.currentItem()
250 def selected_items(self
):
251 return self
.selectedItems()
259 def move(self
, direction
):
260 item
= self
.selected_item()
262 row
= (self
.row(item
) + direction
) % self
.count()
263 elif self
.count() > 0:
264 row
= (self
.count() + direction
) % self
.count()
267 new_item
= self
.item(row
)
269 self
.setCurrentItem(new_item
)
273 def __init__(self
, widget
, Base
):
277 widget
.setAlternatingRowColors(True)
278 widget
.setUniformRowHeights(True)
279 widget
.setAllColumnsShowFocus(True)
280 widget
.setAnimated(True)
281 widget
.setRootIsDecorated(False)
283 def keyPressEvent(self
, event
):
285 Make LeftArrow to work on non-directories.
287 When LeftArrow is pressed on a file entry or an unexpanded
288 directory, then move the current index to the parent directory.
290 This simplifies navigation using the keyboard.
291 For power-users, we support Vim keybindings ;-P
294 # Check whether the item is expanded before calling the base class
295 # keyPressEvent otherwise we end up collapsing and changing the
296 # current index in one shot, which we don't want to do.
298 index
= widget
.currentIndex()
299 was_expanded
= widget
.isExpanded(index
)
300 was_collapsed
= not was_expanded
303 event
= _create_vim_navigation_key_event(event
)
305 # Read the updated event key to take the mappings 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 widget
.isExpanded(index
):
327 index
= widget
.moveCursor(widget
.MoveDown
, event
.modifiers())
328 widget
.setCurrentIndex(index
)
330 # Process non-root entries with valid parents only.
331 elif key
== Qt
.Key_Left
and index
.parent().isValid():
332 # File entries have rowCount() == 0
333 model
= widget
.model()
334 if hasattr(model
, 'itemFromIndex'):
335 item
= model
.itemFromIndex(index
)
336 if hasattr(item
, 'rowCount') and item
.rowCount() == 0:
337 widget
.setCurrentIndex(index
.parent())
339 # Otherwise, do this for collapsed directories only
341 widget
.setCurrentIndex(index
.parent())
343 # If it's a movement key ensure we have a selection
344 elif key
in (Qt
.Key_Left
, Qt
.Key_Up
, Qt
.Key_Right
, Qt
.Key_Down
):
345 # Try to select the first item if the model index is invalid
346 item
= self
.selected_item()
347 if item
is None or not index
.isValid():
348 index
= widget
.model().index(0, 0, QtCore
.QModelIndex())
350 widget
.setCurrentIndex(index
)
354 def item_from_index(self
, item
):
355 """Return a QModelIndex from the provided item"""
356 if hasattr(self
, 'itemFromIndex'):
357 index
= self
.itemFromIndex(item
)
359 index
= self
.model().itemFromIndex()
363 root
= self
.widget
.invisibleRootItem()
365 count
= root
.childCount()
366 return [child(i
) for i
in range(count
)]
368 def selected_items(self
):
369 """Return all selected items"""
371 if hasattr(widget
, 'selectedItems'):
372 return widget
.selectedItems()
373 if hasattr(widget
, 'itemFromIndex'):
374 item_from_index
= widget
.itemFromIndex
376 item_from_index
= widget
.model().itemFromIndex
377 return [item_from_index(i
) for i
in widget
.selectedIndexes()]
379 def selected_item(self
):
380 """Return the first selected item"""
381 selected_items
= self
.selected_items()
382 if not selected_items
:
384 return selected_items
[0]
386 def current_item(self
):
389 if hasattr(widget
, 'currentItem'):
390 item
= widget
.currentItem()
392 index
= widget
.currentIndex()
394 item
= widget
.model().itemFromIndex(index
)
397 def column_widths(self
):
398 """Return the tree's column widths"""
400 count
= widget
.header().count()
401 return [widget
.columnWidth(i
) for i
in range(count
)]
403 def set_column_widths(self
, widths
):
404 """Set the tree's column widths"""
407 count
= widget
.header().count()
408 if len(widths
) > count
:
409 widths
= widths
[:count
]
410 for idx
, value
in enumerate(widths
):
411 widget
.setColumnWidth(idx
, value
)
414 def _create_vim_navigation_key_event(event
):
415 """Support minimal Vim-like keybindings by rewriting the QKeyEvents"""
417 # Remap 'H' to 'Left'
419 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Left
, event
.modifiers())
420 # Remap 'J' to 'Down'
421 elif key
== Qt
.Key_J
:
422 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Down
, event
.modifiers())
424 elif key
== Qt
.Key_K
:
425 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Up
, event
.modifiers())
426 # Remap 'L' to 'Right'
427 elif key
== Qt
.Key_L
:
428 event
= QtGui
.QKeyEvent(event
.type(), Qt
.Key_Right
, event
.modifiers())
432 class DraggableTreeMixin(TreeMixin
):
433 """A tree widget with internal drag+drop reordering of rows
435 Expects that the widget provides an `items_moved` signal.
439 def __init__(self
, widget
, Base
):
440 super().__init
__(widget
, Base
)
442 self
._inner
_drag
= False
443 widget
.setAcceptDrops(True)
444 widget
.setSelectionMode(widget
.SingleSelection
)
445 widget
.setDragEnabled(True)
446 widget
.setDropIndicatorShown(True)
447 widget
.setDragDropMode(QtWidgets
.QAbstractItemView
.InternalMove
)
448 widget
.setSortingEnabled(False)
450 def dragEnterEvent(self
, event
):
451 """Accept internal drags only"""
453 self
.Base
.dragEnterEvent(widget
, event
)
454 self
._inner
_drag
= event
.source() == widget
456 event
.acceptProposedAction()
460 def dragLeaveEvent(self
, event
):
462 self
.Base
.dragLeaveEvent(widget
, event
)
467 self
._inner
_drag
= False
469 def dropEvent(self
, event
):
470 """Re-select selected items after an internal move"""
471 if not self
._inner
_drag
:
475 clicked_items
= self
.selected_items()
476 event
.setDropAction(Qt
.MoveAction
)
477 self
.Base
.dropEvent(widget
, event
)
480 widget
.clearSelection()
481 for item
in clicked_items
:
482 item
.setSelected(True)
483 widget
.items_moved
.emit(clicked_items
)
484 self
._inner
_drag
= False
485 event
.accept() # must be called after dropEvent()
487 def mousePressEvent(self
, event
):
488 """Clear the selection when a mouse click hits no item"""
490 clicked_item
= widget
.itemAt(event
.pos())
491 if clicked_item
is None:
492 widget
.clearSelection()
493 return self
.Base
.mousePressEvent(widget
, event
)
496 class Widget(WidgetMixin
, QtWidgets
.QWidget
):
497 Base
= QtWidgets
.QWidget
499 def __init__(self
, parent
=None):
500 QtWidgets
.QWidget
.__init
__(self
, parent
)
501 WidgetMixin
.__init
__(self
)
504 class Dialog(WidgetMixin
, QtWidgets
.QDialog
):
505 Base
= QtWidgets
.QDialog
507 def __init__(self
, parent
=None):
508 QtWidgets
.QDialog
.__init
__(self
, parent
)
509 WidgetMixin
.__init
__(self
)
510 # Disable the Help button hint on Windows
511 if hasattr(Qt
, 'WindowContextHelpButtonHint'):
512 help_hint
= Qt
.WindowContextHelpButtonHint
513 flags
= self
.windowFlags() & ~help_hint
514 self
.setWindowFlags(flags
)
519 return self
.Base
.accept(self
)
524 return self
.Base
.reject(self
)
527 """Extension method for model de-registration in sub-classes"""
531 """save_settings() is handled by accept() and reject()"""
533 self
.Base
.close(self
)
535 def closeEvent(self
, event
):
536 """save_settings() is handled by accept() and reject()"""
538 self
.Base
.closeEvent(self
, event
)
541 class MainWindow(MainWindowMixin
, QtWidgets
.QMainWindow
):
542 Base
= QtWidgets
.QMainWindow
544 def __init__(self
, parent
=None):
545 QtWidgets
.QMainWindow
.__init
__(self
, parent
)
546 MainWindowMixin
.__init
__(self
)
549 class TreeView(QtWidgets
.QTreeView
):
554 index_about_to_change
= Signal()
556 def __init__(self
, parent
=None):
557 QtWidgets
.QTreeView
.__init
__(self
, parent
)
558 self
._mixin
= self
.Mixin(self
, QtWidgets
.QTreeView
)
560 def keyPressEvent(self
, event
):
561 return self
._mixin
.keyPressEvent(event
)
563 def current_item(self
):
564 return self
._mixin
.current_item()
566 def selected_item(self
):
567 return self
._mixin
.selected_item()
569 def selected_items(self
):
570 return self
._mixin
.selected_items()
573 return self
._mixin
.items()
575 def column_widths(self
):
576 return self
._mixin
.column_widths()
578 def set_column_widths(self
, widths
):
579 return self
._mixin
.set_column_widths(widths
)
582 class TreeWidget(QtWidgets
.QTreeWidget
):
587 index_about_to_change
= Signal()
589 def __init__(self
, parent
=None):
590 super().__init
__(parent
)
591 self
._mixin
= self
.Mixin(self
, QtWidgets
.QTreeWidget
)
593 def keyPressEvent(self
, event
):
594 return self
._mixin
.keyPressEvent(event
)
596 def current_item(self
):
597 return self
._mixin
.current_item()
599 def selected_item(self
):
600 return self
._mixin
.selected_item()
602 def selected_items(self
):
603 return self
._mixin
.selected_items()
606 return self
._mixin
.items()
608 def column_widths(self
):
609 return self
._mixin
.column_widths()
611 def set_column_widths(self
, widths
):
612 return self
._mixin
.set_column_widths(widths
)
615 class DraggableTreeWidget(TreeWidget
):
616 Mixin
= DraggableTreeMixin
617 items_moved
= Signal(object)
619 def mousePressEvent(self
, event
):
620 return self
._mixin
.mousePressEvent(event
)
622 def dropEvent(self
, event
):
623 return self
._mixin
.dropEvent(event
)
625 def dragLeaveEvent(self
, event
):
626 return self
._mixin
.dragLeaveEvent(event
)
628 def dragEnterEvent(self
, event
):
629 return self
._mixin
.dragEnterEvent(event
)
632 class ProgressDialog(QtWidgets
.QProgressDialog
):
633 """Custom progress dialog
635 This dialog ignores the ESC key so that it is not
638 A thread is spawned to animate the progress label text.
642 def __init__(self
, title
, label
, parent
):
643 QtWidgets
.QProgressDialog
.__init
__(self
, parent
)
644 self
._parent
= parent
645 if parent
is not None:
646 self
.setWindowModality(Qt
.WindowModal
)
648 self
.animation_thread
= ProgressAnimationThread(label
, self
)
649 self
.animation_thread
.updated
.connect(self
.set_text
, type=Qt
.QueuedConnection
)
653 self
.setMinimumDuration(0)
654 self
.setCancelButton(None)
655 self
.setFont(qtutils
.default_monospace_font())
656 self
.set_details(title
, label
)
658 def set_details(self
, title
, label
):
659 """Update the window title and progress label"""
660 self
.setWindowTitle(title
)
661 self
.setLabelText(label
+ ' ')
662 self
.animation_thread
.set_text(label
)
664 def set_text(self
, txt
):
665 """Set the label text"""
666 self
.setLabelText(txt
)
668 def keyPressEvent(self
, event
):
669 """Customize keyPressEvent to remove the ESC key cancel feature"""
670 if event
.key() != Qt
.Key_Escape
:
671 super().keyPressEvent(event
)
674 """Start the animation thread and use a wait cursor"""
676 QtWidgets
.QApplication
.setOverrideCursor(Qt
.WaitCursor
)
677 self
.animation_thread
.start()
680 """Stop the animation thread and restore the normal cursor"""
681 self
.animation_thread
.stop()
682 self
.animation_thread
.wait()
683 QtWidgets
.QApplication
.restoreOverrideCursor()
687 class ProgressAnimationThread(QtCore
.QThread
):
688 """Emits a pseudo-animated text stream for progress bars"""
690 # The updated signal is emitted on each tick.
691 updated
= Signal(object)
693 def __init__(self
, txt
, parent
, sleep_time
=0.1):
694 QtCore
.QThread
.__init
__(self
, parent
)
697 self
.sleep_time
= sleep_time
707 def set_text(self
, txt
):
708 """Set the text prefix"""
712 """Tick to the next animated text value"""
713 self
.idx
= (self
.idx
+ 1) % len(self
.symbols
)
714 return self
.txt
+ self
.symbols
[self
.idx
]
717 """Stop the animation thread"""
721 """Emit ticks until stopped"""
724 self
.updated
.emit(self
.tick())
725 time
.sleep(self
.sleep_time
)
728 class ProgressTickThread(QtCore
.QThread
):
729 """Emits an int stream for progress bars"""
731 # The updated signal emits progress tick values.
732 updated
= Signal(int)
733 # The activated signal is emitted when the progress bar is displayed.
743 QtCore
.QThread
.__init
__(self
, parent
)
745 self
.sleep_time
= sleep_time
746 self
.maximum
= maximum
747 self
.start_time
= start_time
752 """Cycle to the next tick value
754 Returned values are in the inclusive (0, maximum + 1) range.
756 self
.value
= (self
.value
+ self
.step
) % (self
.maximum
+ 1)
757 if self
.value
== self
.maximum
:
759 elif self
.value
== 0:
764 """Stop the tick thread and reset to the initial state"""
770 """Start the tick thread
772 The progress bar will not be activated until after the start_time
773 interval has elapsed.
775 initial_time
= time
.time()
782 self
.updated
.emit(self
.tick())
785 if self
.start_time
< (now
- initial_time
):
787 self
.activated
.emit()
788 time
.sleep(self
.sleep_time
)
791 class SpinBox(QtWidgets
.QSpinBox
):
793 self
, parent
=None, value
=None, mini
=1, maxi
=99999, step
=0, prefix
='', suffix
=''
795 QtWidgets
.QSpinBox
.__init
__(self
, parent
)
796 self
.setPrefix(prefix
)
797 self
.setSuffix(suffix
)
798 self
.setWrapping(True)
799 self
.setMinimum(mini
)
800 self
.setMaximum(maxi
)
802 self
.setSingleStep(step
)
803 if value
is not None:
805 text_width
= qtutils
.text_width(self
.font(), 'MMMMMM')
806 width
= max(self
.minimumWidth(), text_width
)
807 self
.setMinimumWidth(width
)
810 class DirectoryPathLineEdit(QtWidgets
.QWidget
):
811 """A combined line edit and file browser button"""
813 def __init__(self
, path
, parent
):
814 QtWidgets
.QWidget
.__init
__(self
, parent
)
816 self
.line_edit
= QtWidgets
.QLineEdit()
817 self
.line_edit
.setText(path
)
819 self
.browse_button
= qtutils
.create_button(
820 tooltip
=N_('Select directory'), icon
=icons
.folder()
822 layout
= qtutils
.hbox(
828 self
.setLayout(layout
)
830 qtutils
.connect_button(self
.browse_button
, self
._select
_directory
)
832 def set_value(self
, value
):
833 """Set the path value"""
834 self
.line_edit
.setText(value
)
837 """Return the current path value"""
838 return self
.line_edit
.text().strip()
840 def _select_directory(self
):
841 """Open a file browser and select a directory"""
842 output_dir
= qtutils
.opendir_dialog(N_('Select directory'), self
.value())
845 # Make the directory relative only if it the current directory or
846 # or subdirectory from the current directory.
847 current_dir
= core
.getcwd()
848 if output_dir
== current_dir
:
850 elif output_dir
.startswith(current_dir
+ os
.sep
):
851 output_dir
= os
.path
.relpath(output_dir
)
852 self
.set_value(output_dir
)
855 def export_header_columns(widget
, state
):
856 """Save QHeaderView column sizes"""
858 header
= widget
.horizontalHeader()
859 for idx
in range(header
.count()):
860 columns
.append(header
.sectionSize(idx
))
862 state
['columns'] = columns
865 def apply_header_columns(widget
, state
):
866 """Apply QHeaderView column sizes"""
867 columns
= mklist(state
.get('columns', []))
868 header
= widget
.horizontalHeader()
869 if header
.stretchLastSection():
870 # Setting the size will make the section wider than necessary, which
871 # defeats the purpose of the stretch flag. Skip the last column when
872 # it's stretchy so that it retains the stretchy behavior.
873 columns
= columns
[:-1]
874 for idx
, size
in enumerate(columns
):
875 header
.resizeSection(idx
, size
)
878 class MessageBox(Dialog
):
879 """Improved QMessageBox replacement
881 QMessageBox has a lot of usability issues. It sometimes cannot be
882 resized, and it brings along a lots of annoying properties that we'd have
883 to workaround, so we use a simple custom dialog instead.
901 Dialog
.__init
__(self
, parent
=parent
)
904 self
.setWindowModality(Qt
.WindowModal
)
906 self
.setWindowTitle(title
)
908 self
.logo_label
= QtWidgets
.QLabel()
910 # Render into a 1-inch wide pixmap
911 pixmap
= logo
.pixmap(defs
.large_icon
)
912 self
.logo_label
.setPixmap(pixmap
)
914 self
.logo_label
.hide()
916 self
.text_label
= QtWidgets
.QLabel()
917 self
.text_label
.setText(text
)
919 self
.info_label
= QtWidgets
.QLabel()
921 self
.info_label
.setText(info
)
923 self
.info_label
.hide()
925 ok_icon
= icons
.mkicon(ok_icon
, icons
.ok
)
926 self
.button_ok
= qtutils
.create_button(text
=ok_text
, icon
=ok_icon
)
928 self
.button_close
= qtutils
.close_button(text
=cancel_text
, icon
=cancel_icon
)
931 self
.button_ok
.setText(ok_text
)
933 self
.button_ok
.hide()
935 self
.details_text
= QtWidgets
.QPlainTextEdit()
936 self
.details_text
.setReadOnly(True)
938 self
.details_text
.setFont(qtutils
.default_monospace_font())
939 self
.details_text
.setPlainText(details
)
941 self
.details_text
.hide()
943 self
.info_layout
= qtutils
.vbox(
951 self
.top_layout
= qtutils
.hbox(
959 self
.buttons_layout
= qtutils
.hbox(
967 self
.main_layout
= qtutils
.vbox(
974 self
.main_layout
.setStretchFactor(self
.details_text
, 2)
975 self
.setLayout(self
.main_layout
)
978 self
.button_ok
.setDefault(True)
979 self
.button_ok
.setFocus()
981 self
.button_close
.setDefault(True)
982 self
.button_close
.setFocus()
984 qtutils
.connect_button(self
.button_ok
, self
.accept
)
985 qtutils
.connect_button(self
.button_close
, self
.reject
)
986 self
.init_state(None, self
.set_initial_size
)
988 def set_initial_size(self
):
989 width
= defs
.dialog_w
990 height
= defs
.msgbox_h
991 self
.resize(width
, height
)
993 def keyPressEvent(self
, event
):
994 """Handle Y/N hotkeys"""
997 QtCore
.QTimer
.singleShot(0, self
.accept
)
998 elif key
in (Qt
.Key_N
, Qt
.Key_Q
):
999 QtCore
.QTimer
.singleShot(0, self
.reject
)
1000 elif key
== Qt
.Key_Tab
:
1001 if self
.button_ok
.isVisible():
1003 if self
.focusWidget() == self
.button_close
:
1004 self
.button_ok
.setFocus()
1006 self
.button_close
.setFocus()
1008 Dialog
.keyPressEvent(self
, event
)
1014 def apply_state(self
, state
):
1015 """Imports data for view save/restore"""
1016 desktop_width
, desktop_height
= qtutils
.desktop_size()
1017 width
= min(desktop_width
, utils
.asint(state
.get('width')))
1018 height
= min(desktop_height
, utils
.asint(state
.get('height')))
1019 x
= min(desktop_width
, utils
.asint(state
.get('x')))
1020 y
= min(desktop_height
, utils
.asint(state
.get('y')))
1023 if width
and height
:
1024 self
.resize(width
, height
)
1030 def export_state(self
):
1031 """Exports data for view save/restore"""
1032 desktop_width
, desktop_height
= qtutils
.desktop_size()
1034 state
['width'] = min(desktop_width
, self
.width())
1035 state
['height'] = min(desktop_height
, self
.height())
1036 state
['x'] = min(desktop_width
, self
.x())
1037 state
['y'] = min(desktop_height
, self
.y())
1051 """Confirm that an action should take place"""
1052 cancel_text
= cancel_text
or N_('Cancel')
1053 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
1056 parent
=qtutils
.active_window(),
1059 info
=informative_text
,
1062 cancel_text
=cancel_text
,
1063 cancel_icon
=cancel_icon
,
1068 return mbox
.run() == mbox
.Accepted
1071 def critical(title
, message
=None, details
=None):
1072 """Show a warning with the provided title and message."""
1075 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxCritical
)
1077 parent
=qtutils
.active_window(),
1086 def command_error(title
, cmd
, status
, out
, err
):
1087 """Report an error message about a failed command"""
1088 details
= Interaction
.format_out_err(out
, err
)
1089 message
= Interaction
.format_command_status(cmd
, status
)
1090 critical(title
, message
=message
, details
=details
)
1093 def information(title
, message
=None, details
=None, informative_text
=None):
1094 """Show information with the provided title and message."""
1098 parent
=qtutils
.active_window(),
1101 info
=informative_text
,
1108 def progress(title
, text
, parent
):
1109 """Create a new ProgressDialog"""
1110 return ProgressDialog(title
, text
, parent
)
1113 class ProgressBar(QtWidgets
.QProgressBar
):
1114 """An indeterminate progress bar with animated scrolling"""
1116 def __init__(self
, parent
, maximum
, hide
=(), disable
=(), visible
=False):
1117 super().__init
__(parent
)
1118 self
.setTextVisible(False)
1119 self
.setMaximum(maximum
)
1121 self
.setVisible(False)
1122 self
.progress_thread
= ProgressTickThread(self
, maximum
)
1123 self
.progress_thread
.updated
.connect(self
.setValue
, type=Qt
.QueuedConnection
)
1124 self
.progress_thread
.activated
.connect(self
.activate
, type=Qt
.QueuedConnection
)
1125 self
._widgets
_to
_hide
= hide
1126 self
._widgets
_to
_disable
= disable
1129 """Start the progress tick thread"""
1130 QtWidgets
.QApplication
.setOverrideCursor(Qt
.WaitCursor
)
1132 for widget
in self
._widgets
_to
_disable
:
1133 widget
.setEnabled(False)
1135 self
.progress_thread
.start()
1138 """Hide widgets and display the progress bar"""
1139 for widget
in self
._widgets
_to
_hide
:
1144 """Stop the progress tick thread, re-enable and display widgets"""
1145 self
.progress_thread
.stop()
1146 self
.progress_thread
.wait()
1148 for widget
in self
._widgets
_to
_disable
:
1149 widget
.setEnabled(True)
1152 for widget
in self
._widgets
_to
_hide
:
1155 QtWidgets
.QApplication
.restoreOverrideCursor()
1158 def progress_bar(parent
, maximum
=10, hide
=(), disable
=()):
1159 """Return a text-less progress bar"""
1160 widget
= ProgressBar(parent
, maximum
, hide
=hide
, disable
=disable
)
1164 def question(title
, text
, default
=True, logo
=None):
1165 """Launches a QMessageBox question with the provided title and message.
1166 Passing "default=False" will make "No" the default choice."""
1167 parent
= qtutils
.active_window()
1169 logo
= icons
.from_style(QtWidgets
.QStyle
.SP_MessageBoxQuestion
)
1170 msgbox
= MessageBox(
1177 cancel_text
=N_('No'),
1179 return msgbox
.run() == msgbox
.Accepted
1182 def save_as(filename
, title
):
1183 return qtutils
.save_as(filename
, title
=title
)
1186 def async_command(title
, cmd
, runtask
):
1187 task
= qtutils
.SimpleTask(partial(core
.run_command
, cmd
))
1188 task
.connect(partial(async_command_result
, title
, cmd
))
1192 def async_command_result(title
, cmd
, result
):
1193 status
, out
, err
= result
1194 cmd_string
= core
.list2cmdline(cmd
)
1195 Interaction
.command(title
, cmd_string
, status
, out
, err
)
1199 """Install the GUI-model interaction hooks"""
1200 Interaction
.critical
= staticmethod(critical
)
1201 Interaction
.confirm
= staticmethod(confirm
)
1202 Interaction
.question
= staticmethod(question
)
1203 Interaction
.information
= staticmethod(information
)
1204 Interaction
.command_error
= staticmethod(command_error
)
1205 Interaction
.save_as
= staticmethod(save_as
)
1206 Interaction
.async_command
= staticmethod(async_command
)