1 from __future__
import division
, absolute_import
, unicode_literals
6 from PyQt4
import QtGui
7 from PyQt4
import QtCore
8 from PyQt4
.QtCore
import Qt
9 from PyQt4
.QtCore
import SIGNAL
10 from PyQt4
.QtGui
import QDockWidget
13 from cola
import gitcfg
14 from cola
import qtcompat
15 from cola
import qtutils
16 from cola
.settings
import Settings
19 class WidgetMixin(object):
20 """Mix-in for common utilities and serialization of widget state"""
22 def __init__(self
, QtClass
):
23 self
.QtClass
= QtClass
24 self
._apply
_state
_applied
= False
27 """Automatically centers dialogs"""
28 if not self
._apply
_state
_applied
and self
.parent() is not None:
29 left
= self
.parent().x()
30 width
= self
.parent().width()
31 center_x
= left
+ width
//2
33 x
= center_x
- self
.width()//2
37 # Call the base Qt show()
38 return self
.QtClass
.show(self
)
41 """Returns the name of the view class"""
42 return self
.__class
__.__name
__.lower()
44 def save_state(self
, settings
=None):
48 if gitcfg
.current().get('cola.savewindowsettings', True):
49 settings
.save_gui_state(self
)
51 def restore_state(self
, settings
=None):
55 state
= settings
.get_gui_state(self
)
56 return bool(state
) and self
.apply_state(state
)
58 def apply_state(self
, state
):
59 """Imports data for view save/restore"""
62 self
.resize(state
['width'], state
['height'])
66 self
.move(state
['x'], state
['y'])
70 if state
['maximized']:
74 self
._apply
_state
_applied
= result
77 def export_state(self
):
78 """Exports data for view save/restore"""
79 state
= self
.windowState()
80 maximized
= bool(state
& Qt
.WindowMaximized
)
84 'width': self
.width(),
85 'height': self
.height(),
86 'maximized': maximized
,
89 def save_settings(self
):
92 settings
.add_recent(core
.getcwd())
93 return self
.save_state(settings
=settings
)
95 def closeEvent(self
, event
):
97 self
.QtClass
.closeEvent(self
, event
)
100 class MainWindowMixin(WidgetMixin
):
102 def __init__(self
, QtClass
):
103 WidgetMixin
.__init
__(self
, QtClass
)
105 self
.dockwidgets
= []
106 self
.lock_layout
= False
107 self
.widget_version
= 0
108 qtcompat
.set_common_dock_options(self
)
110 def export_state(self
):
111 """Exports data for save/restore"""
112 state
= WidgetMixin
.export_state(self
)
113 windowstate
= self
.saveState(self
.widget_version
)
114 state
['lock_layout'] = self
.lock_layout
115 state
['windowstate'] = windowstate
.toBase64().data().decode('ascii')
118 def apply_state(self
, state
):
119 result
= WidgetMixin
.apply_state(self
, state
)
120 windowstate
= state
.get('windowstate', None)
121 if windowstate
is None:
124 result
= self
.restoreState(QtCore
.QByteArray
.fromBase64(str(windowstate
)),
125 self
.widget_version
) and result
126 self
.lock_layout
= state
.get('lock_layout', self
.lock_layout
)
127 self
.update_dockwidget_lock_state()
128 self
.update_dockwidget_tooltips()
131 def set_lock_layout(self
, lock_layout
):
132 self
.lock_layout
= lock_layout
133 self
.update_dockwidget_lock_state()
135 def update_dockwidget_lock_state(self
):
137 features
= (QDockWidget
.DockWidgetClosable |
138 QDockWidget
.DockWidgetFloatable
)
140 features
= (QDockWidget
.DockWidgetClosable |
141 QDockWidget
.DockWidgetFloatable |
142 QDockWidget
.DockWidgetMovable
)
143 for widget
in self
.dockwidgets
:
144 widget
.titleBarWidget().update_tooltips()
145 widget
.setFeatures(features
)
147 def update_dockwidget_tooltips(self
):
148 for widget
in self
.dockwidgets
:
149 widget
.titleBarWidget().update_tooltips()
151 def closeEvent(self
, event
):
152 qtutils
.persist_clipboard()
153 WidgetMixin
.closeEvent(self
, event
)
156 class TreeMixin(object):
158 def __init__(self
, QtClass
):
159 self
.QtClass
= QtClass
160 self
.setAlternatingRowColors(True)
161 self
.setUniformRowHeights(True)
162 self
.setAllColumnsShowFocus(True)
163 self
.setAnimated(True)
164 self
.setRootIsDecorated(False)
166 def keyPressEvent(self
, event
):
168 Make LeftArrow to work on non-directories.
170 When LeftArrow is pressed on a file entry or an unexpanded
171 directory, then move the current index to the parent directory.
173 This simplifies navigation using the keyboard.
174 For power-users, we support Vim keybindings ;-P
177 # Check whether the item is expanded before calling the base class
178 # keyPressEvent otherwise we end up collapsing and changing the
179 # current index in one shot, which we don't want to do.
180 index
= self
.currentIndex()
181 was_expanded
= self
.isExpanded(index
)
182 was_collapsed
= not was_expanded
185 # Rewrite the event before marshalling to QTreeView.event()
188 # Remap 'H' to 'Left'
190 event
= QtGui
.QKeyEvent(event
.type(),
193 # Remap 'J' to 'Down'
194 elif key
== Qt
.Key_J
:
195 event
= QtGui
.QKeyEvent(event
.type(),
199 elif key
== Qt
.Key_K
:
200 event
= QtGui
.QKeyEvent(event
.type(),
203 # Remap 'L' to 'Right'
204 elif key
== Qt
.Key_L
:
205 event
= QtGui
.QKeyEvent(event
.type(),
209 # Re-read the event key to take the remappings into account
212 idxs
= self
.selectedIndexes()
213 rows
= [idx
.row() for idx
in idxs
]
214 if len(rows
) == 1 and rows
[0] == 0:
215 # The cursor is at the beginning of the line.
216 # If we have selection then simply reset the cursor.
217 # Otherwise, emit a signal so that the parent can
219 self
.emit(SIGNAL('up()'))
221 elif key
== Qt
.Key_Space
:
222 self
.emit(SIGNAL('space()'))
224 result
= self
.QtClass
.keyPressEvent(self
, event
)
226 # Let others hook in here before we change the indexes
227 self
.emit(SIGNAL('indexAboutToChange()'))
229 # Automatically select the first entry when expanding a directory
230 if (key
== Qt
.Key_Right
and was_collapsed
and
231 self
.isExpanded(index
)):
232 index
= self
.moveCursor(self
.MoveDown
, event
.modifiers())
233 self
.setCurrentIndex(index
)
235 # Process non-root entries with valid parents only.
236 elif key
== Qt
.Key_Left
and index
.parent().isValid():
238 # File entries have rowCount() == 0
239 if self
.model().itemFromIndex(index
).rowCount() == 0:
240 self
.setCurrentIndex(index
.parent())
242 # Otherwise, do this for collapsed directories only
244 self
.setCurrentIndex(index
.parent())
246 # If it's a movement key ensure we have a selection
247 elif key
in (Qt
.Key_Left
, Qt
.Key_Up
, Qt
.Key_Right
, Qt
.Key_Down
):
248 # Try to select the first item if the model index is invalid
249 item
= self
.selected_item()
250 if item
is None or not index
.isValid():
251 index
= self
.model().index(0, 0, QtCore
.QModelIndex())
253 self
.setCurrentIndex(index
)
258 root
= self
.invisibleRootItem()
260 count
= root
.childCount()
261 return [child(i
) for i
in range(count
)]
263 def selected_items(self
):
264 """Return all selected items"""
265 if hasattr(self
, 'selectedItems'):
266 return self
.selectedItems()
268 item_from_index
= self
.model().itemFromIndex
269 return [item_from_index(i
) for i
in self
.selectedIndexes()]
271 def selected_item(self
):
272 """Return the first selected item"""
273 selected_items
= self
.selected_items()
274 if not selected_items
:
276 return selected_items
[0]
278 def current_item(self
):
279 if hasattr(self
, 'currentItem'):
280 item
= self
.currentItem()
282 index
= self
.currentIndex()
284 item
= self
.model().itemFromIndex(index
)
290 class DraggableTreeMixin(TreeMixin
):
291 """A tree widget with internal drag+drop reordering of rows"""
293 ITEMS_MOVED_SIGNAL
= 'items_moved'
295 def __init__(self
, QtClass
):
296 super(DraggableTreeMixin
, self
).__init
__(QtClass
)
297 self
.setAcceptDrops(True)
298 self
.setSelectionMode(self
.SingleSelection
)
299 self
.setDragEnabled(True)
300 self
.setDropIndicatorShown(True)
301 self
.setDragDropMode(QtGui
.QAbstractItemView
.InternalMove
)
302 self
.setSortingEnabled(False)
303 self
._inner
_drag
= False
305 def dragEnterEvent(self
, event
):
306 """Accept internal drags only"""
307 self
.QtClass
.dragEnterEvent(self
, event
)
308 self
._inner
_drag
= event
.source() == self
310 event
.acceptProposedAction()
314 def dragLeaveEvent(self
, event
):
315 self
.QtClass
.dragLeaveEvent(self
, event
)
320 self
._inner
_drag
= False
322 def dropEvent(self
, event
):
323 """Re-select selected items after an internal move"""
324 if not self
._inner
_drag
:
327 clicked_items
= self
.selected_items()
328 event
.setDropAction(Qt
.MoveAction
)
329 self
.QtClass
.dropEvent(self
, event
)
332 self
.clearSelection()
333 for item
in clicked_items
:
334 self
.setItemSelected(item
, True)
335 self
.emit(SIGNAL(self
.ITEMS_MOVED_SIGNAL
), clicked_items
)
336 self
._inner
_drag
= False
337 event
.accept() # must be called after dropEvent()
339 def mousePressEvent(self
, event
):
340 """Clear the selection when a mouse click hits no item"""
341 clicked_item
= self
.itemAt(event
.pos())
342 if clicked_item
is None:
343 self
.clearSelection()
344 return self
.QtClass
.mousePressEvent(self
, event
)
347 class Widget(WidgetMixin
, QtGui
.QWidget
):
349 def __init__(self
, parent
=None):
350 QtGui
.QWidget
.__init
__(self
, parent
)
351 WidgetMixin
.__init
__(self
, QtGui
.QWidget
)
354 class Dialog(WidgetMixin
, QtGui
.QDialog
):
356 def __init__(self
, parent
=None, save_settings
=False):
357 QtGui
.QDialog
.__init
__(self
, parent
)
358 WidgetMixin
.__init
__(self
, QtGui
.QDialog
)
359 self
._save
_settings
= save_settings
362 if self
._save
_settings
:
364 return self
.QtClass
.close(self
)
367 if self
._save
_settings
:
369 return self
.QtClass
.reject(self
)
372 class MainWindow(MainWindowMixin
, QtGui
.QMainWindow
):
374 def __init__(self
, parent
=None):
375 QtGui
.QMainWindow
.__init
__(self
, parent
)
376 MainWindowMixin
.__init
__(self
, QtGui
.QMainWindow
)
379 class TreeView(TreeMixin
, QtGui
.QTreeView
):
381 def __init__(self
, parent
=None):
382 QtGui
.QTreeView
.__init
__(self
, parent
)
383 TreeMixin
.__init
__(self
, QtGui
.QTreeView
)
386 class TreeWidget(TreeMixin
, QtGui
.QTreeWidget
):
388 def __init__(self
, parent
=None):
389 QtGui
.QTreeWidget
.__init
__(self
, parent
)
390 TreeMixin
.__init
__(self
, QtGui
.QTreeWidget
)
393 class DraggableTreeWidget(DraggableTreeMixin
, QtGui
.QTreeWidget
):
395 def __init__(self
, parent
=None):
396 QtGui
.QTreeWidget
.__init
__(self
, parent
)
397 DraggableTreeMixin
.__init
__(self
, QtGui
.QTreeWidget
)
400 class ProgressDialog(QtGui
.QProgressDialog
):
401 """Custom progress dialog
403 This dialog ignores the ESC key so that it is not
406 An thread is spawned to animate the progress label text.
409 def __init__(self
, title
, label
, parent
):
410 QtGui
.QProgressDialog
.__init
__(self
, parent
)
411 self
.setFont(qtutils
.diff_font())
413 self
.setCancelButton(None)
414 if parent
is not None:
415 self
.setWindowModality(Qt
.WindowModal
)
416 self
.progress_thread
= ProgressAnimationThread(label
, self
)
417 self
.connect(self
.progress_thread
,
418 SIGNAL('update_progress(PyQt_PyObject)'),
419 self
.update_progress
, Qt
.QueuedConnection
)
421 self
.set_details(title
, label
)
423 def set_details(self
, title
, label
):
424 self
.setWindowTitle(title
)
425 self
.setLabelText(label
+ ' ')
426 self
.progress_thread
.set_text(label
)
428 def update_progress(self
, txt
):
429 self
.setLabelText(txt
)
431 def keyPressEvent(self
, event
):
432 if event
.key() != Qt
.Key_Escape
:
433 QtGui
.QProgressDialog
.keyPressEvent(self
, event
)
436 QtGui
.QApplication
.setOverrideCursor(Qt
.WaitCursor
)
437 self
.progress_thread
.start()
438 QtGui
.QProgressDialog
.show(self
)
441 QtGui
.QApplication
.restoreOverrideCursor()
442 self
.progress_thread
.stop()
443 self
.progress_thread
.wait()
444 QtGui
.QProgressDialog
.hide(self
)
447 class ProgressAnimationThread(QtCore
.QThread
):
448 """Emits a pseudo-animated text stream for progress bars"""
450 def __init__(self
, txt
, parent
, timeout
=0.1):
451 QtCore
.QThread
.__init
__(self
, parent
)
454 self
.timeout
= timeout
464 def set_text(self
, txt
):
468 self
.idx
= (self
.idx
+ 1) % len(self
.symbols
)
469 return self
.txt
+ self
.symbols
[self
.idx
]
477 self
.emit(SIGNAL('update_progress(PyQt_PyObject)'), self
.next())
478 time
.sleep(self
.timeout
)
481 class SpinBox(QtGui
.QSpinBox
):
482 def __init__(self
, parent
=None):
483 QtGui
.QSpinBox
.__init
__(self
, parent
)
485 self
.setMaximum(99999)