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
17 from cola
.widgets
import defs
20 class WidgetMixin(object):
21 """Mix-in for common utilities and serialization of widget state"""
23 def __init__(self
, QtClass
):
24 self
.QtClass
= QtClass
25 self
._apply
_state
_applied
= False
28 """Automatically centers dialogs"""
29 if not self
._apply
_state
_applied
and self
.parent() is not None:
30 left
= self
.parent().x()
31 width
= self
.parent().width()
32 center_x
= left
+ width
//2
34 x
= center_x
- self
.width()//2
38 # Call the base Qt show()
39 return self
.QtClass
.show(self
)
42 """Returns the name of the view class"""
43 return self
.__class
__.__name
__.lower()
45 def save_state(self
, settings
=None):
49 if gitcfg
.current().get('cola.savewindowsettings', True):
50 settings
.save_gui_state(self
)
52 def restore_state(self
, settings
=None):
56 state
= settings
.get_gui_state(self
)
57 return bool(state
) and self
.apply_state(state
)
59 def apply_state(self
, state
):
60 """Imports data for view save/restore"""
63 self
.resize(state
['width'], state
['height'])
67 self
.move(state
['x'], state
['y'])
71 if state
['maximized']:
75 self
._apply
_state
_applied
= result
78 def export_state(self
):
79 """Exports data for view save/restore"""
80 state
= self
.windowState()
81 maximized
= bool(state
& Qt
.WindowMaximized
)
85 'width': self
.width(),
86 'height': self
.height(),
87 'maximized': maximized
,
90 def save_settings(self
):
93 settings
.add_recent(core
.getcwd())
94 return self
.save_state(settings
=settings
)
96 def closeEvent(self
, event
):
98 self
.QtClass
.closeEvent(self
, event
)
101 class MainWindowMixin(WidgetMixin
):
103 def __init__(self
, QtClass
):
104 WidgetMixin
.__init
__(self
, QtClass
)
106 self
.dockwidgets
= []
107 self
.lock_layout
= False
108 self
.widget_version
= 0
109 qtcompat
.set_common_dock_options(self
)
111 def export_state(self
):
112 """Exports data for save/restore"""
113 state
= WidgetMixin
.export_state(self
)
114 windowstate
= self
.saveState(self
.widget_version
)
115 state
['lock_layout'] = self
.lock_layout
116 state
['windowstate'] = windowstate
.toBase64().data().decode('ascii')
119 def apply_state(self
, state
):
120 result
= WidgetMixin
.apply_state(self
, state
)
121 windowstate
= state
.get('windowstate', None)
122 if windowstate
is None:
125 from_base64
= QtCore
.QByteArray
.fromBase64
126 result
= self
.restoreState(
127 from_base64(str(windowstate
)),
128 self
.widget_version
) and result
129 self
.lock_layout
= state
.get('lock_layout', self
.lock_layout
)
130 self
.update_dockwidget_lock_state()
131 self
.update_dockwidget_tooltips()
134 def set_lock_layout(self
, lock_layout
):
135 self
.lock_layout
= lock_layout
136 self
.update_dockwidget_lock_state()
138 def update_dockwidget_lock_state(self
):
140 features
= (QDockWidget
.DockWidgetClosable |
141 QDockWidget
.DockWidgetFloatable
)
143 features
= (QDockWidget
.DockWidgetClosable |
144 QDockWidget
.DockWidgetFloatable |
145 QDockWidget
.DockWidgetMovable
)
146 for widget
in self
.dockwidgets
:
147 widget
.titleBarWidget().update_tooltips()
148 widget
.setFeatures(features
)
150 def update_dockwidget_tooltips(self
):
151 for widget
in self
.dockwidgets
:
152 widget
.titleBarWidget().update_tooltips()
155 class TreeMixin(object):
157 def __init__(self
, QtClass
):
158 self
.QtClass
= QtClass
159 self
.setAlternatingRowColors(True)
160 self
.setUniformRowHeights(True)
161 self
.setAllColumnsShowFocus(True)
162 self
.setAnimated(True)
163 self
.setRootIsDecorated(False)
165 def keyPressEvent(self
, event
):
167 Make LeftArrow to work on non-directories.
169 When LeftArrow is pressed on a file entry or an unexpanded
170 directory, then move the current index to the parent directory.
172 This simplifies navigation using the keyboard.
173 For power-users, we support Vim keybindings ;-P
176 # Check whether the item is expanded before calling the base class
177 # keyPressEvent otherwise we end up collapsing and changing the
178 # current index in one shot, which we don't want to do.
179 index
= self
.currentIndex()
180 was_expanded
= self
.isExpanded(index
)
181 was_collapsed
= not was_expanded
184 # Rewrite the event before marshalling to QTreeView.event()
187 # Remap 'H' to 'Left'
189 event
= QtGui
.QKeyEvent(event
.type(),
192 # Remap 'J' to 'Down'
193 elif key
== Qt
.Key_J
:
194 event
= QtGui
.QKeyEvent(event
.type(),
198 elif key
== Qt
.Key_K
:
199 event
= QtGui
.QKeyEvent(event
.type(),
202 # Remap 'L' to 'Right'
203 elif key
== Qt
.Key_L
:
204 event
= QtGui
.QKeyEvent(event
.type(),
208 # Re-read the event key to take the remappings into account
211 idxs
= self
.selectedIndexes()
212 rows
= [idx
.row() for idx
in idxs
]
213 if len(rows
) == 1 and rows
[0] == 0:
214 # The cursor is at the beginning of the line.
215 # If we have selection then simply reset the cursor.
216 # Otherwise, emit a signal so that the parent can
218 self
.emit(SIGNAL('up()'))
220 elif key
== Qt
.Key_Space
:
221 self
.emit(SIGNAL('space()'))
223 result
= self
.QtClass
.keyPressEvent(self
, event
)
225 # Let others hook in here before we change the indexes
226 self
.emit(SIGNAL('indexAboutToChange()'))
228 # Automatically select the first entry when expanding a directory
229 if (key
== Qt
.Key_Right
and was_collapsed
and
230 self
.isExpanded(index
)):
231 index
= self
.moveCursor(self
.MoveDown
, event
.modifiers())
232 self
.setCurrentIndex(index
)
234 # Process non-root entries with valid parents only.
235 elif key
== Qt
.Key_Left
and index
.parent().isValid():
237 # File entries have rowCount() == 0
238 if self
.model().itemFromIndex(index
).rowCount() == 0:
239 self
.setCurrentIndex(index
.parent())
241 # Otherwise, do this for collapsed directories only
243 self
.setCurrentIndex(index
.parent())
245 # If it's a movement key ensure we have a selection
246 elif key
in (Qt
.Key_Left
, Qt
.Key_Up
, Qt
.Key_Right
, Qt
.Key_Down
):
247 # Try to select the first item if the model index is invalid
248 item
= self
.selected_item()
249 if item
is None or not index
.isValid():
250 index
= self
.model().index(0, 0, QtCore
.QModelIndex())
252 self
.setCurrentIndex(index
)
257 root
= self
.invisibleRootItem()
259 count
= root
.childCount()
260 return [child(i
) for i
in range(count
)]
262 def selected_items(self
):
263 """Return all selected items"""
264 if hasattr(self
, 'selectedItems'):
265 return self
.selectedItems()
267 item_from_index
= self
.model().itemFromIndex
268 return [item_from_index(i
) for i
in self
.selectedIndexes()]
270 def selected_item(self
):
271 """Return the first selected item"""
272 selected_items
= self
.selected_items()
273 if not selected_items
:
275 return selected_items
[0]
277 def current_item(self
):
278 if hasattr(self
, 'currentItem'):
279 item
= self
.currentItem()
281 index
= self
.currentIndex()
283 item
= self
.model().itemFromIndex(index
)
289 class DraggableTreeMixin(TreeMixin
):
290 """A tree widget with internal drag+drop reordering of rows"""
292 ITEMS_MOVED_SIGNAL
= 'items_moved'
294 def __init__(self
, QtClass
):
295 super(DraggableTreeMixin
, self
).__init
__(QtClass
)
296 self
.setAcceptDrops(True)
297 self
.setSelectionMode(self
.SingleSelection
)
298 self
.setDragEnabled(True)
299 self
.setDropIndicatorShown(True)
300 self
.setDragDropMode(QtGui
.QAbstractItemView
.InternalMove
)
301 self
.setSortingEnabled(False)
302 self
._inner
_drag
= False
304 def dragEnterEvent(self
, event
):
305 """Accept internal drags only"""
306 self
.QtClass
.dragEnterEvent(self
, event
)
307 self
._inner
_drag
= event
.source() == self
309 event
.acceptProposedAction()
313 def dragLeaveEvent(self
, event
):
314 self
.QtClass
.dragLeaveEvent(self
, event
)
319 self
._inner
_drag
= False
321 def dropEvent(self
, event
):
322 """Re-select selected items after an internal move"""
323 if not self
._inner
_drag
:
326 clicked_items
= self
.selected_items()
327 event
.setDropAction(Qt
.MoveAction
)
328 self
.QtClass
.dropEvent(self
, event
)
331 self
.clearSelection()
332 for item
in clicked_items
:
333 self
.setItemSelected(item
, True)
334 self
.emit(SIGNAL(self
.ITEMS_MOVED_SIGNAL
), clicked_items
)
335 self
._inner
_drag
= False
336 event
.accept() # must be called after dropEvent()
338 def mousePressEvent(self
, event
):
339 """Clear the selection when a mouse click hits no item"""
340 clicked_item
= self
.itemAt(event
.pos())
341 if clicked_item
is None:
342 self
.clearSelection()
343 return self
.QtClass
.mousePressEvent(self
, event
)
346 class Widget(WidgetMixin
, QtGui
.QWidget
):
348 def __init__(self
, parent
=None):
349 QtGui
.QWidget
.__init
__(self
, parent
)
350 WidgetMixin
.__init
__(self
, QtGui
.QWidget
)
353 class Dialog(WidgetMixin
, QtGui
.QDialog
):
355 def __init__(self
, parent
=None, save_settings
=False):
356 QtGui
.QDialog
.__init
__(self
, parent
)
357 WidgetMixin
.__init
__(self
, QtGui
.QDialog
)
358 self
._save
_settings
= save_settings
361 if self
._save
_settings
:
363 return self
.QtClass
.close(self
)
366 if self
._save
_settings
:
368 return self
.QtClass
.reject(self
)
371 class MainWindow(MainWindowMixin
, QtGui
.QMainWindow
):
373 def __init__(self
, parent
=None):
374 QtGui
.QMainWindow
.__init
__(self
, parent
)
375 MainWindowMixin
.__init
__(self
, QtGui
.QMainWindow
)
376 self
.setStyleSheet("""
377 QMainWindow::separator {
378 width: %(separator)spx;
379 height: %(separator)spx;
381 QMainWindow::separator:hover {
384 """ % dict(separator
=defs
.separator
))
387 class TreeView(TreeMixin
, QtGui
.QTreeView
):
389 def __init__(self
, parent
=None):
390 QtGui
.QTreeView
.__init
__(self
, parent
)
391 TreeMixin
.__init
__(self
, QtGui
.QTreeView
)
394 class TreeWidget(TreeMixin
, QtGui
.QTreeWidget
):
396 def __init__(self
, parent
=None):
397 QtGui
.QTreeWidget
.__init
__(self
, parent
)
398 TreeMixin
.__init
__(self
, QtGui
.QTreeWidget
)
401 class DraggableTreeWidget(DraggableTreeMixin
, QtGui
.QTreeWidget
):
403 def __init__(self
, parent
=None):
404 QtGui
.QTreeWidget
.__init
__(self
, parent
)
405 DraggableTreeMixin
.__init
__(self
, QtGui
.QTreeWidget
)
408 class ProgressDialog(QtGui
.QProgressDialog
):
409 """Custom progress dialog
411 This dialog ignores the ESC key so that it is not
414 An thread is spawned to animate the progress label text.
417 def __init__(self
, title
, label
, parent
):
418 QtGui
.QProgressDialog
.__init
__(self
, parent
)
419 self
.setFont(qtutils
.diff_font())
421 self
.setCancelButton(None)
422 if parent
is not None:
423 self
.setWindowModality(Qt
.WindowModal
)
424 self
.progress_thread
= ProgressAnimationThread(label
, self
)
425 self
.connect(self
.progress_thread
,
426 SIGNAL('update_progress(PyQt_PyObject)'),
427 self
.update_progress
, Qt
.QueuedConnection
)
429 self
.set_details(title
, label
)
432 def set_details(self
, title
, label
):
433 self
.setWindowTitle(title
)
434 self
.setLabelText(label
+ ' ')
435 self
.progress_thread
.set_text(label
)
437 def update_progress(self
, txt
):
438 self
.setLabelText(txt
)
440 def keyPressEvent(self
, event
):
441 if event
.key() != Qt
.Key_Escape
:
442 QtGui
.QProgressDialog
.keyPressEvent(self
, event
)
445 QtGui
.QApplication
.setOverrideCursor(Qt
.WaitCursor
)
446 self
.progress_thread
.start()
447 QtGui
.QProgressDialog
.show(self
)
450 QtGui
.QApplication
.restoreOverrideCursor()
451 self
.progress_thread
.stop()
452 self
.progress_thread
.wait()
453 QtGui
.QProgressDialog
.hide(self
)
456 class ProgressAnimationThread(QtCore
.QThread
):
457 """Emits a pseudo-animated text stream for progress bars"""
459 def __init__(self
, txt
, parent
, timeout
=0.1):
460 QtCore
.QThread
.__init
__(self
, parent
)
463 self
.timeout
= timeout
473 def set_text(self
, txt
):
477 self
.idx
= (self
.idx
+ 1) % len(self
.symbols
)
478 return self
.txt
+ self
.symbols
[self
.idx
]
486 self
.emit(SIGNAL('update_progress(PyQt_PyObject)'), self
.cycle())
487 time
.sleep(self
.timeout
)
490 class SpinBox(QtGui
.QSpinBox
):
491 def __init__(self
, parent
=None):
492 QtGui
.QSpinBox
.__init
__(self
, parent
)
494 self
.setMaximum(99999)