views.main: automatically expand top-level items
[git-cola.git] / cola / views / main.py
blob98140d601e35041c0497b3085f3d229c2b468edc
1 """This view provides the main git-cola user interface.
2 """
4 from PyQt4 import QtGui
5 from PyQt4 import QtCore
6 from PyQt4.QtCore import Qt
7 from PyQt4.QtCore import SIGNAL
9 from cola import qtutils
10 from cola.views.syntax import DiffSyntaxHighlighter
11 from cola.views.mainwindow import MainWindow
13 class MainView(MainWindow):
14 """The main cola interface."""
15 IDX_HEADER = -1
16 IDX_STAGED = 0
17 IDX_MODIFIED = 1
18 IDX_UNMERGED = 2
19 IDX_UNTRACKED = 3
20 IDX_END = 4
22 def __init__(self, parent=None):
23 MainWindow.__init__(self, parent)
24 self.amend_is_checked = self.amend_radio.isChecked
25 self.action_undo = self.commitmsg.undo
26 self.action_redo = self.commitmsg.redo
27 self.action_paste = self.commitmsg.paste
28 self.action_select_all = self.commitmsg.selectAll
30 # Qt does not support noun/verbs
31 self.commit_button.setText(qtutils.tr('Commit@@verb'))
32 self.commit_menu.setTitle(qtutils.tr('Commit@@verb'))
34 # Default to creating a new commit(i.e. not an amend commit)
35 self.new_commit_radio.setChecked(True)
37 # Diff/patch syntax highlighter
38 self.syntax = DiffSyntaxHighlighter(self.display_text.document())
40 # Display the current column
41 self.connect(self.commitmsg,
42 SIGNAL('cursorPositionChanged()'),
43 self.show_current_column)
45 # Initialize the seen tree widget indexes
46 self._seen_indexes = set()
48 # Initialize the GUI to show 'Column: 00'
49 self.show_current_column()
51 # Internal field used by import/export_state().
52 # Change this whenever dockwidgets are removed.
53 self._widget_version = 1
55 def set_staged(self, items, check=True):
56 """Adds items to the 'Staged' subtree."""
57 self._set_subtree(items, self.IDX_STAGED, staged=True, check=check)
59 def set_modified(self, items):
60 """Adds items to the 'Modified' subtree."""
61 self._set_subtree(items, self.IDX_MODIFIED)
63 def set_unmerged(self, items):
64 """Adds items to the 'Unmerged' subtree."""
65 self._set_subtree(items, self.IDX_UNMERGED)
67 def set_untracked(self, items):
68 """Adds items to the 'Untracked' subtree."""
69 self._set_subtree(items, self.IDX_UNTRACKED)
71 def _set_subtree(self, items, idx,
72 staged=False, untracked=False, check=True):
73 parent = self.status_tree.topLevelItem(idx)
74 parent.takeChildren()
75 for item in items:
76 treeitem = qtutils.create_treeitem(item,
77 staged=staged,
78 check=check,
79 untracked=untracked)
80 parent.addChild(treeitem)
81 if idx not in self._seen_indexes and items:
82 self._seen_indexes.add(idx)
83 self.expand_status()
84 if items:
85 self.status_tree.setItemHidden(parent, False)
86 else:
87 self.status_tree.setItemHidden(parent, True)
89 def set_display(self, text):
90 """Set the diff text display."""
91 if text is not None:
92 self.display_text.setText(text)
94 def expand_status(self):
95 for idx in xrange(0, self.IDX_END):
96 item = self.status_tree.topLevelItem(idx)
97 if item:
98 self.status_tree.expandItem(item)
100 def index_for_item(self, item):
101 """Given an item, returns the index of the item.
102 The indexes for unstaged items are grouped such that
103 the index of unmerged[1] = len(modified) + 1, etc.
105 if not item:
106 return False, -1
107 parent = item.parent()
108 if not parent:
109 return False, -1
110 tree = self.status_tree
111 pidx = tree.indexOfTopLevelItem(parent)
112 if pidx == self.IDX_STAGED:
113 return True, parent.indexOfChild(item)
114 elif pidx == self.IDX_MODIFIED:
115 return False, parent.indexOfChild(item)
117 count = tree.topLevelItem(self.IDX_MODIFIED).childCount()
118 if pidx == self.IDX_UNMERGED:
119 return False, count + parent.indexOfChild(item)
121 count += tree.topLevelItem(self.IDX_UNMERGED).childCount()
122 if pidx == self.IDX_UNTRACKED:
123 return False, count + parent.indexOfChild(item)
125 return False, -1
127 def selection(self):
128 tree = self.status_tree
129 item = tree.currentItem()
130 if not item:
131 return -1, False
132 parent = item.parent()
133 if not parent:
134 return -1, False
136 idx = parent.indexOfChild(item)
137 pidx = tree.indexOfTopLevelItem(parent)
139 if pidx == self.IDX_STAGED or pidx == self.IDX_MODIFIED:
140 return idx, tree.isItemSelected(item)
142 elif pidx == self.IDX_UNMERGED:
143 num_modified = tree.topLevelItem(self.IDX_MODIFIED).childCount()
144 return idx + num_modified, tree.isItemSelected(item)
146 elif pidx == self.IDX_UNTRACKED:
147 num_modified = tree.topLevelItem(self.IDX_MODIFIED).childCount()
148 num_unmerged = tree.topLevelItem(self.IDX_UNMERGED).childCount()
149 return idx + num_modified + num_unmerged, tree.isItemSelected(item)
150 return -1, False
152 def staged_item(self, itemidx):
153 return self._subtree_item(self.IDX_STAGED, itemidx)
155 def modified_item(self, itemidx):
156 return self._subtree_item(self.IDX_MODIFIED, itemidx)
158 def unstaged_item(self, itemidx):
159 tree = self.status_tree
160 # is it modified?
161 item = tree.topLevelItem(self.IDX_MODIFIED)
162 count = item.childCount()
163 if itemidx < count:
164 return item.child(itemidx)
165 # is it unmerged?
166 item = tree.topLevelItem(self.IDX_UNMERGED)
167 count += item.childCount()
168 if itemidx < count:
169 return item.child(itemidx)
170 # is it untracked?
171 item = tree.topLevelItem(self.IDX_UNTRACKED)
172 count += item.childCount()
173 if itemidx < count:
174 return item.child(itemidx)
175 # Nope..
176 return None
178 def _subtree_item(self, idx, itemidx):
179 parent = self.status_tree.topLevelItem(idx)
180 return parent.child(itemidx)
182 def unstaged(self, items):
183 tree = self.status_tree
184 num_modified = tree.topLevelItem(self.IDX_MODIFIED).childCount()
185 num_unmerged = tree.topLevelItem(self.IDX_UNMERGED).childCount()
186 modified = self.modified(items)
187 unmerged = self.unmerged(items[num_modified:])
188 untracked = self.untracked(items[num_modified+num_unmerged:])
189 return modified + unmerged + untracked
191 def staged(self, items):
192 return self._subtree_selection(self.IDX_STAGED, items)
194 def modified(self, items):
195 return self._subtree_selection(self.IDX_MODIFIED, items)
197 def unmerged(self, items):
198 return self._subtree_selection(self.IDX_UNMERGED, items)
200 def untracked(self, items):
201 return self._subtree_selection(self.IDX_UNTRACKED, items)
203 def _subtree_selection(self, idx, items):
204 item = self.status_tree.topLevelItem(idx)
205 return qtutils.tree_selection(item, items)
207 def show(self):
208 """Override base show to set icons and expand top-level items."""
209 result = MainWindow.show(self)
210 staged = self.status_tree.topLevelItem(self.IDX_STAGED)
211 staged.setIcon(0, qtutils.icon('plus.png'))
213 modified = self.status_tree.topLevelItem(self.IDX_MODIFIED)
214 modified.setIcon(0, qtutils.icon('modified.png'))
216 unmerged = self.status_tree.topLevelItem(self.IDX_UNMERGED)
217 unmerged.setIcon(0, qtutils.icon('unmerged.png'))
219 untracked = self.status_tree.topLevelItem(self.IDX_UNTRACKED)
220 untracked.setIcon(0, qtutils.icon('untracked.png'))
222 # Set the diff font
223 qtutils.set_diff_font(self.display_text)
225 self.status_tree.expandToDepth(0)
226 return result
228 def enter_diff_mode(self, text):
230 Enter diff mode; changes the 'Staged' header to 'Changed'.
232 This also enables the 'Exit <Mode> Mode' button.
233 `text` is the message displayed on the button.
236 staged = self.status_tree.topLevelItem(self.IDX_STAGED)
237 staged.setText(0, self.tr('Changed'))
238 self.alt_button.setText(self.tr(text))
239 self.alt_button.show()
241 def exit_diff_mode(self):
243 Exit diff mode; changes the 'Changed' header to 'Staged'.
245 This also hides the 'Exit Diff Mode' button.
248 staged = self.status_tree.topLevelItem(self.IDX_STAGED)
249 staged.setText(0, self.tr('Staged'))
250 self.alt_button.hide()
251 self.reset_display()
253 def action_cut(self):
254 self.action_copy()
255 self.action_delete()
257 def action_copy(self):
258 cursor = self.commitmsg.textCursor()
259 selection = cursor.selection().toPlainText()
260 qtutils.set_clipboard(selection)
262 def action_delete(self):
263 self.commitmsg.textCursor().removeSelectedText()
265 def reset_checkboxes(self):
266 self.new_commit_radio.setChecked(True)
267 self.amend_radio.setChecked(False)
269 def reset_display(self):
270 self.set_display('')
272 def copy_display(self):
273 cursor = self.display_text.textCursor()
274 selection = cursor.selection().toPlainText()
275 qtutils.set_clipboard(selection)
277 def diff_selection(self):
278 cursor = self.display_text.textCursor()
279 offset = cursor.position()
280 selection = unicode(cursor.selection().toPlainText())
281 return offset, selection
283 def tree_selection(self):
284 """Returns a list of (category, row) representing the tree selection."""
285 selected = self.status_tree.selectedIndexes()
286 result = []
287 for idx in selected:
288 if idx.parent().isValid():
289 parent_idx = idx.parent()
290 entry = (parent_idx.row(), idx.row())
291 else:
292 entry = (-1, idx.row())
293 result.append(entry)
294 return result
296 def selected_line(self):
297 cursor = self.display_text.textCursor()
298 offset = cursor.position()
299 contents = unicode(self.display_text.toPlainText())
300 while (offset >= 1
301 and contents[offset-1]
302 and contents[offset-1] != '\n'):
303 offset -= 1
304 data = contents[offset:]
305 if '\n' in data:
306 line, rest = data.split('\n', 1)
307 else:
308 line = data
309 return line
311 def display(self, text):
312 self.set_display(text)
314 def show_current_column(self):
315 cursor = self.commitmsg.textCursor()
316 colnum = cursor.columnNumber()
317 self.column_label.setText('Column: %02d' % colnum)
319 def import_state(self, state):
320 """Imports data for save/restore"""
321 MainWindow.import_state(self, state)
322 # Restore the dockwidget, etc. window state
323 if 'windowstate' in state:
324 windowstate = state['windowstate']
325 self.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
326 self._widget_version)
328 def export_state(self):
329 """Exports data for save/restore"""
330 state = MainWindow.export_state(self)
331 # Save the window state
332 windowstate = self.saveState(self._widget_version)
333 state['windowstate'] = unicode(windowstate.toBase64().data())
334 return state