Merge lp:~jameinel/bzr-explorer/wt_model into lp:~bzr-explorer-dev/bzr-explorer/trunk.old
- wt_model
- Merge into trunk.old
Status: | Rejected |
---|---|
Rejected by: | Alexander Belchenko |
Proposed branch: | lp:~jameinel/bzr-explorer/wt_model |
Merge into: | lp:~bzr-explorer-dev/bzr-explorer/trunk.old |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~jameinel/bzr-explorer/wt_model |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Bazaar Explorer Developers | Pending | ||
Review via email: mp+8488@code.launchpad.net |
Commit message
Description of the change
John A Meinel (jameinel) wrote : | # |
Alexander Belchenko (bialix) wrote : | # |
I think the current approach is to use treewidget from qbzr. I think this won't be merged.
John A Meinel (jameinel) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Alexander Belchenko wrote:
> I think the current approach is to use treewidget from qbzr. I think this won't be merged.
Feel free to reject it, I'm only getting an OOPS when I try to visit the
LP page.
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAku
d3YAnAxuk7IV9rd
=IvWj
-----END PGP SIGNATURE-----
Unmerged revisions
- 181. By John A Meinel
-
We now pick a different bunch of columns.
It actually looks pretty interesting now.
- 180. By John A Meinel
-
Include the full set of values that are given from iter_changes.
- 179. By John A Meinel
-
Switch to using WT.iter_
changes( WT.basis_ tree()) This gives us more status information, which makes it more interesting
overall. - 178. By John A Meinel
-
Add a decoration for column 0 from the QDirModel
- 177. By John A Meinel
-
Merge basic-tests updates
- 176. By John A Meinel
-
Start trying to add pixmaps for the WTModel.
- 175. By John A Meinel
-
Fix a couple edge cases in the wt_browser.py that were causing issues
- 174. By John A Meinel
-
Update the WorkingTreeModel to have a genuinely artificial root item.
- 173. By John A Meinel
-
Hack in the new model to get it viewable.
It would seem I need to re-think the root object.
It seems views have to have an indexable object to view.
Which means that we need an arbitrary root object, and then
the root-id of the filesystem will be a child of that. - 172. By John A Meinel
-
Implement support for WTModel.data(), I believe this makes a complete model.
Preview Diff
1 | === modified file '__init__.py' |
2 | --- __init__.py 2009-06-30 10:55:21 +0000 |
3 | +++ __init__.py 2009-07-07 19:39:48 +0000 |
4 | @@ -29,7 +29,7 @@ |
5 | lazy_import(globals(), ''' |
6 | from PyQt4 import QtGui, QtCore |
7 | |
8 | -from bzrlib.plugins.explorer.lib.explorer import QExplorerMainWindow |
9 | +from bzrlib.plugins.explorer.lib import explorer |
10 | ''') |
11 | |
12 | |
13 | @@ -38,15 +38,9 @@ |
14 | import bzrlib.plugins.qbzr.lib |
15 | |
16 | |
17 | -def test_suite(): |
18 | - """Called by bzrlib to fetch tests for this plugin""" |
19 | - from unittest import TestSuite, TestLoader |
20 | - #from bzrlib.plugins.explorer.lib.tests import ... |
21 | - loader = TestLoader() |
22 | - suite = TestSuite() |
23 | - for module in []: |
24 | - suite.addTests(loader.loadTestsFromModule(module)) |
25 | - return suite |
26 | +def load_tests(basic_tests, module, loader): |
27 | + from bzrlib.plugins.explorer.tests import load_tests |
28 | + return load_tests(basic_tests, module, loader) |
29 | |
30 | |
31 | class cmd_explorer(Command): |
32 | @@ -114,7 +108,7 @@ |
33 | # TODO: setup exception handler ala QBzrCommand |
34 | # (inheriting directly from QBzrCommands cause a metadata error?) |
35 | app = QtGui.QApplication(sys.argv) |
36 | - window = QExplorerMainWindow(location=location, profile=hat, |
37 | + window = explorer.QExplorerMainWindow(location=location, profile=hat, |
38 | desktop=desktop, dry_run=dry_run, preferences=prefs) |
39 | window.show() |
40 | app.exec_() |
41 | |
42 | === modified file 'lib/wt_browser.py' |
43 | --- lib/wt_browser.py 2009-07-02 06:50:18 +0000 |
44 | +++ lib/wt_browser.py 2009-07-07 22:37:42 +0000 |
45 | @@ -19,6 +19,9 @@ |
46 | from PyQt4 import QtCore, QtGui |
47 | |
48 | from bzrlib.plugins.explorer.lib.i18n import gettext, N_ |
49 | +from bzrlib.plugins.explorer.lib import ( |
50 | + wt_model, |
51 | + ) |
52 | from bzrlib.plugins.explorer.lib.kinds import ( |
53 | OPEN_FOLDER_ACTION, |
54 | EDIT_ACTION, |
55 | @@ -39,21 +42,22 @@ |
56 | |
57 | def _build_ui(self): |
58 | # Init them model and view |
59 | - model = QtGui.QDirModel() |
60 | + from bzrlib import workingtree |
61 | + model = wt_model.WorkingTreeModel(workingtree.WorkingTree.open('.')) |
62 | # sort flags for tree view: direcotries first, |
63 | # on Windows items are case-insensitive |
64 | flags = QtCore.QDir.SortFlags(QtCore.QDir.Name|QtCore.QDir.DirsFirst) |
65 | if sys.platform == 'win32': |
66 | flags |= QtCore.QDir.IgnoreCase |
67 | - model.setSorting(flags) |
68 | + # model.setSorting(flags) |
69 | view = QtGui.QTreeView() |
70 | view.setModel(model) |
71 | |
72 | # Hide all except the Name column |
73 | - for i in range(1, model.columnCount()): |
74 | - view.setColumnHidden(i, True) |
75 | - if getattr(view, 'setHeaderHidden', None): |
76 | - view.setHeaderHidden(True) |
77 | + # for i in range(1, model.columnCount()): |
78 | + # view.setColumnHidden(i, True) |
79 | + # if getattr(view, 'setHeaderHidden', None): |
80 | + # view.setHeaderHidden(True) |
81 | |
82 | # Build a button bar for the view |
83 | browse_button = QtGui.QPushButton() |
84 | @@ -131,6 +135,7 @@ |
85 | self._action_callback("open", unicode(path)) |
86 | |
87 | def do_current_changed(self, model_index, prev_model_index): |
88 | + return |
89 | model = model_index.model() |
90 | fileinfo = model.fileInfo(model_index) |
91 | self._edit_button.setEnabled(fileinfo and not fileinfo.isDir()) |
92 | @@ -147,7 +152,7 @@ |
93 | else: |
94 | # Show the tree |
95 | self.stacked.setCurrentIndex(1) |
96 | - index = self.model.index(path) |
97 | + index = self.model.index(0, 0, None) |
98 | self.view.setRootIndex(index) |
99 | self._root = path |
100 | self._selected_fileinfo = None |
101 | |
102 | === added file 'lib/wt_model.py' |
103 | --- lib/wt_model.py 1970-01-01 00:00:00 +0000 |
104 | +++ lib/wt_model.py 2009-07-09 19:46:58 +0000 |
105 | @@ -0,0 +1,269 @@ |
106 | +# Copyright (C) 2009 Canonical Ltd |
107 | +# |
108 | +# This program is free software; you can redistribute it and/or modify |
109 | +# it under the terms of the GNU General Public License as published by |
110 | +# the Free Software Foundation; either version 2 of the License, or |
111 | +# (at your option) any later version. |
112 | +# |
113 | +# This program is distributed in the hope that it will be useful, |
114 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
115 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
116 | +# GNU General Public License for more details. |
117 | +# |
118 | +# You should have received a copy of the GNU General Public License |
119 | +# along with this program; if not, write to the Free Software |
120 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
121 | + |
122 | +"""Implement a Model defining interactions with a WorkingTree""" |
123 | + |
124 | +from bzrlib import osutils, workingtree |
125 | + |
126 | +from PyQt4 import QtCore, QtGui |
127 | + |
128 | + |
129 | +# TODO: consider that this is similar to what one would want for an |
130 | +# InventoryModel, except we should have extra attributes like |
131 | +# "is_modified", etc. Also consider that for a working tree, we probably |
132 | +# need to handle files that are unknown and ignored. |
133 | +class _WTItem(object): |
134 | + """Encapsulates the display information for a given item. |
135 | + |
136 | + :ivar children: |
137 | + :type children: A list of _WTItem objects |
138 | + """ |
139 | + |
140 | + _versioned_map = {(True, True): ' ', # versioned in both |
141 | + (True, False): '-', # versioned in source |
142 | + (False, True): '+', # versioned in target |
143 | + (False, False): '-', # versioned in neither |
144 | + } |
145 | + |
146 | + def __init__(self, file_id, is_modified, path, name, kind, versioned, |
147 | + executable, parent_id, old_path, old_name, old_kind, |
148 | + was_versioned, old_executable, old_parent_id, parent, |
149 | + ignore_pattern): |
150 | + """Create a new _WTItem |
151 | + """ |
152 | + self.file_id = file_id |
153 | + self.path = path |
154 | + self.name = name |
155 | + self.kind = kind |
156 | + self.versioned = versioned |
157 | + self.is_modified = is_modified |
158 | + self.ignore_pattern = ignore_pattern |
159 | + self.executable = executable |
160 | + self.parent_id = parent_id |
161 | + self.old_path = old_path |
162 | + self.old_kind = old_kind |
163 | + self.old_name = old_name |
164 | + self.was_versioned = was_versioned |
165 | + self.old_executable = old_executable |
166 | + self.old_parent_id = old_parent_id |
167 | + self.parent = parent |
168 | + # Note: this creates a cyclical reference, consider using parent |
169 | + # instead |
170 | + self.children = [] |
171 | + # Determine the status of this file |
172 | + self.status = None |
173 | + if self.versioned is not None and self.was_versioned is not None: |
174 | + self._determine_status() |
175 | + |
176 | + def _determine_status(self): |
177 | + if self.ignore_pattern is not None: |
178 | + self.status = 'I ' |
179 | + return |
180 | + # Generally copied from bzrlib.delta.report_changes |
181 | + exe_status = ' ' |
182 | + # files are "renamed" if they are moved or if name changes, as long |
183 | + # as it had a value |
184 | + if (self.old_name is not None and self.name is not None |
185 | + and self.old_parent_id is not None |
186 | + and self.parent_id is not None |
187 | + and (self.old_name != self.name |
188 | + or self.parent_id != self.old_parent_id)): |
189 | + versioned_status = 'R' # Renamed implies versioned in both |
190 | + else: |
191 | + versioned_status = self._versioned_map[ |
192 | + (self.was_versioned, self.versioned)] |
193 | + if self.old_kind != self.kind: |
194 | + if self.old_kind is None: |
195 | + modified_status = 'N' # created |
196 | + elif self.kind is None: |
197 | + modified_status = 'D' # deleted |
198 | + else: |
199 | + modified_status = 'K' # kind changed |
200 | + else: |
201 | + if self.is_modified: |
202 | + modified_status = 'M' # modified |
203 | + else: |
204 | + modified_status = ' ' # unchanged |
205 | + if self.kind == 'file': |
206 | + if (self.executable != self.old_executable): |
207 | + exe_status = '*' |
208 | + self.status = modified_status + versioned_status + exe_status |
209 | + |
210 | + def __repr__(self): |
211 | + return '%s(%s)' % (self.__class__.__name__, |
212 | + self.__dict__) |
213 | + |
214 | + def row(self): |
215 | + """Return the row for this object in the parents children list.""" |
216 | + if self.parent is None: |
217 | + return 0 |
218 | + return self.parent.children.index(self) |
219 | + |
220 | + def _from_iter_changes(self, wt, changes): |
221 | + dir_by_id = {} |
222 | + dir_by_path = {} |
223 | + for (file_id, paths, did_change, versions, parent_ids, names, kinds, |
224 | + executables) in changes: |
225 | + if file_id is None: |
226 | + # not versioned |
227 | + parent_dir = osutils.dirname(paths[1]) |
228 | + if names[1] == '': # unversioned root? |
229 | + raise ValueError('not supported yet') |
230 | + parent = dir_by_path[parent_dir] |
231 | + ignore_pattern = wt.is_ignored(paths[1]) |
232 | + else: |
233 | + if parent_ids[1] is None: # Root entry |
234 | + # Or maybe this happens for unversioned files? |
235 | + assert names[1] == '' |
236 | + parent = self |
237 | + else: |
238 | + parent = dir_by_id[parent_ids[1]] |
239 | + ignore_pattern = None |
240 | + item = _WTItem(file_id, did_change, paths[1], names[1], kinds[1], |
241 | + versions[1], executables[1], parent_ids[1], |
242 | + paths[0], names[0], kinds[0], versions[0], |
243 | + executables[0], parent_ids[0], parent, |
244 | + ignore_pattern) |
245 | + parent.children.append(item) |
246 | + if kinds[1] == 'directory': |
247 | + dir_by_id[file_id] = item |
248 | + dir_by_path[paths[1]] = item |
249 | + |
250 | + @classmethod |
251 | + def create_from_wt(cls, wt): |
252 | + """Create the _WTItem tree structure from an inventory.""" |
253 | + wt.lock_read() |
254 | + try: |
255 | + basis_tree = wt.basis_tree() |
256 | + basis_tree.lock_read() |
257 | + try: |
258 | + root_wt_item = cls(*([None]*16)) |
259 | + changes = wt.iter_changes(basis_tree, include_unchanged=True, |
260 | + require_versioned=False, want_unversioned=True) |
261 | + root_wt_item._from_iter_changes(wt, changes) |
262 | + finally: |
263 | + basis_tree.unlock() |
264 | + finally: |
265 | + wt.unlock() |
266 | + return root_wt_item |
267 | + |
268 | + |
269 | +class WorkingTreeModel(QtCore.QAbstractItemModel): |
270 | + |
271 | + _headers = [ |
272 | + 'name', |
273 | + 'kind', |
274 | + 'status', |
275 | + 'path', |
276 | + ] |
277 | + _column_to_attribute = dict(enumerate(_headers)) |
278 | + |
279 | + def __init__(self, wt, parent=None): |
280 | + QtCore.QAbstractItemModel.__init__(self, parent) |
281 | + self._wt = wt |
282 | + self._dirmodel = QtGui.QDirModel() |
283 | + self._root_wt_item = _WTItem.create_from_wt(self._wt) |
284 | + |
285 | + def index(self, row, column, parent=None): |
286 | + """Return the QModelIndex for the given item. |
287 | + |
288 | + :param row: In this case, the Nth file for the given directory |
289 | + :type row: int |
290 | + :param column: The data attribute for the given file/dir |
291 | + :type column: int |
292 | + :param parent: the containing directory |
293 | + :type parent: QModelIndex |
294 | + :return: The index object to reference the given data |
295 | + :rtype: QModelIndex |
296 | + """ |
297 | + if parent is None: |
298 | + parent = QtCore.QModelIndex() |
299 | + parent_item = self._root_wt_item |
300 | + if not self.hasIndex(row, column, parent): |
301 | + return QtCore.QModelIndex() |
302 | + |
303 | + if not parent.isValid(): |
304 | + parent_item = self._root_wt_item |
305 | + else: |
306 | + parent_item = parent.internalPointer() |
307 | + child = parent_item.children[row] |
308 | + return self.createIndex(row, column, child) |
309 | + |
310 | + def parent(self, index): |
311 | + if index is None or not index.isValid(): |
312 | + return QtCore.QModelIndex() |
313 | + |
314 | + item = index.internalPointer() |
315 | + if item.parent is None or item.parent is self._root_wt_item: |
316 | + return QtCore.QModelIndex() |
317 | + return self.createIndex(item.parent.row(), 0, item.parent) |
318 | + |
319 | + def rowCount(self, parent=None): |
320 | + if parent is None or not parent.isValid(): |
321 | + parent_item = self._root_wt_item |
322 | + else: |
323 | + parent_item = parent.internalPointer() |
324 | + return len(parent_item.children) |
325 | + |
326 | + # implement def hasChildren(self, ...) if rowCount is expensive |
327 | + # We could do this, by looking at Item.kind |
328 | + # For now, though, we don't lazily load anything, so leave it alone |
329 | + |
330 | + def columnCount(self, parent=None): |
331 | + # this can vary based on parent, but *doesn't* |
332 | + return len(WorkingTreeModel._headers) |
333 | + |
334 | + def headerData(self, section, orientation, role): |
335 | + """Return the header to show for a given entry.""" |
336 | + headers = WorkingTreeModel._headers |
337 | + if (orientation == QtCore.Qt.Horizontal |
338 | + and role == QtCore.Qt.DisplayRole |
339 | + and section < len(headers)): |
340 | + return QtCore.QVariant(headers[section]) |
341 | + return QtCore.QVariant() |
342 | + |
343 | + def flags(self, index): |
344 | + if index is None or not index.isValid(): |
345 | + # This should really be a QtCore.Qt.ItemFlags object, not sure how |
346 | + # to cast it into one, though |
347 | + return 0 |
348 | + return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) |
349 | + |
350 | + def data(self, index, role): |
351 | + if index is None or not index.isValid(): |
352 | + return QtCore.QVariant() |
353 | + # TODO: implement uspport for Qt.DecorationRole, if we return a |
354 | + # QPixmap, then we will get the displayed icon. |
355 | + item = index.internalPointer() |
356 | + if role == QtCore.Qt.DecorationRole and index.column() == 0: |
357 | + # Return the decoration from an QDirModel |
358 | + dir_index = self._dirmodel.index(item.path) |
359 | + data = self._dirmodel.data(dir_index, role) |
360 | + return data |
361 | + if role != QtCore.Qt.DisplayRole: |
362 | + return QtCore.QVariant() |
363 | + attr = self._column_to_attribute[index.column()] |
364 | + return QtCore.QVariant(getattr(item, attr, None)) |
365 | + |
366 | + # implement def setData(self, ...) if we want to be editable |
367 | + |
368 | + # Interesting Role list: |
369 | + # QtCore.Qt.DisplayRole, QtCore.Qt.ToolTipRole, QtCore.Qt.WhatsThisRole |
370 | + |
371 | + # Do we want to incrementally populate? Perhaps this would be useful for an |
372 | + # IterChangesModel |
373 | + # def fetchMore(self, ...) |
374 | + # def canFetchmore(self, ...) |
375 | |
376 | === added directory 'tests' |
377 | === added file 'tests/__init__.py' |
378 | --- tests/__init__.py 1970-01-01 00:00:00 +0000 |
379 | +++ tests/__init__.py 2009-07-09 17:44:53 +0000 |
380 | @@ -0,0 +1,88 @@ |
381 | +# Copyright (C) 2009 Canonical Ltd |
382 | +# |
383 | +# This program is free software; you can redistribute it and/or modify |
384 | +# it under the terms of the GNU General Public License as published by |
385 | +# the Free Software Foundation; either version 2 of the License, or |
386 | +# (at your option) any later version. |
387 | +# |
388 | +# This program is distributed in the hope that it will be useful, |
389 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
390 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
391 | +# GNU General Public License for more details. |
392 | +# |
393 | +# You should have received a copy of the GNU General Public License |
394 | +# along with this program; if not, write to the Free Software |
395 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
396 | + |
397 | +"""Test Suite for bzr-explorer""" |
398 | + |
399 | +from bzrlib.tests import ( |
400 | + TestCase, |
401 | + TestCaseInTempDir, |
402 | + TestCaseWithTransport, |
403 | + ) |
404 | + |
405 | + |
406 | +class TestCaseWithQt(TestCaseWithTransport): |
407 | + """Many GUI functions require a QApplication to be running. |
408 | + |
409 | + This class ensures a QtApplication is running during setUp. |
410 | + """ |
411 | + |
412 | + _app = None |
413 | + |
414 | + def setUp(self): |
415 | + super(TestCaseWithQt, self).setUp() |
416 | + self.ensureApp() |
417 | + self.logger = self.createLogger() |
418 | + |
419 | + def ensureApp(self): |
420 | + if TestCaseWithQt._app is None: |
421 | + from PyQt4 import QtGui |
422 | + TestCaseWithQt._app = QtGui.QApplication(['bzr-explorer-test-app']) |
423 | + # It seems we can leave it running. |
424 | + |
425 | + def createLogger(self, *args): |
426 | + from PyQt4 import QtCore |
427 | + |
428 | + class ConnectionLogger(QtCore.QObject): |
429 | + |
430 | + def __init__(self, *args): |
431 | + QtCore.QObject.__init__(self, *args) |
432 | + # A log of actions that have occurrred |
433 | + self.log = [] |
434 | + |
435 | + def createNamedSlot(self, signal_name): |
436 | + """Create a new slot, which tracks the signal name given. |
437 | + |
438 | + :param signal_name: The name of the signal we are connecting |
439 | + :return: A function that can be used as a signal to this logger. |
440 | + """ |
441 | + def named_slot(*args): |
442 | + self.log.append((signal_name,) + args) |
443 | + return named_slot |
444 | + |
445 | + def connectToNamedSlot(self, signal, source): |
446 | + """Create a new named slot, and connect the signal from source. |
447 | + |
448 | + :param signal: The value to pass to QtCore.SIGNAL, this will |
449 | + also be the name logged when the signal is activated. |
450 | + :param source: An object that can generate the given signal. |
451 | + """ |
452 | + slot = self.createNamedSlot(signal) |
453 | + QtCore.QObject.connect(source, QtCore.SIGNAL(signal), slot) |
454 | + |
455 | + return ConnectionLogger(*args) |
456 | + |
457 | + |
458 | +def load_tests(basic_tests, module, loader): |
459 | + suite = loader.suiteClass() |
460 | + suite.addTests(basic_tests) |
461 | + |
462 | + mod_names = [__name__ + '.' + x for x in [ |
463 | + 'test_test_suite', |
464 | + 'test_welcome', |
465 | + 'test_wt_model', |
466 | + ]] |
467 | + suite.addTests(loader.loadTestsFromModuleNames(mod_names)) |
468 | + return suite |
469 | |
470 | === added file 'tests/test_test_suite.py' |
471 | --- tests/test_test_suite.py 1970-01-01 00:00:00 +0000 |
472 | +++ tests/test_test_suite.py 2009-07-08 20:28:10 +0000 |
473 | @@ -0,0 +1,66 @@ |
474 | +# Copyright (C) 2009 Canonical Ltd |
475 | +# |
476 | +# This program is free software; you can redistribute it and/or modify |
477 | +# it under the terms of the GNU General Public License as published by |
478 | +# the Free Software Foundation; either version 2 of the License, or |
479 | +# (at your option) any later version. |
480 | +# |
481 | +# This program is distributed in the hope that it will be useful, |
482 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
483 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
484 | +# GNU General Public License for more details. |
485 | +# |
486 | +# You should have received a copy of the GNU General Public License |
487 | +# along with this program; if not, write to the Free Software |
488 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
489 | + |
490 | +"""Tests for the testing infrastructure in bzr-explorer.""" |
491 | + |
492 | +from PyQt4 import QtCore, QtGui |
493 | + |
494 | +from bzrlib.plugins.explorer import tests |
495 | + |
496 | + |
497 | +class _Emitter(QtCore.QObject): |
498 | + |
499 | + def trigger_foo(self): |
500 | + self.emit(QtCore.SIGNAL('foo()')) |
501 | + |
502 | + def trigger_bar(self, arg1, arg2): |
503 | + self.emit(QtCore.SIGNAL('bar(int, int)'), arg1, arg2) |
504 | + |
505 | + |
506 | +class TestTestCaseWithQt(tests.TestCaseWithQt): |
507 | + |
508 | + def test_create_qpixmap(self): |
509 | + pixmap = QtGui.QPixmap() |
510 | + |
511 | + def test_logger_named_slot(self): |
512 | + slot = self.logger.createNamedSlot('trapping_foo') |
513 | + emitter = _Emitter() |
514 | + QtCore.QObject.connect(emitter, QtCore.SIGNAL('foo()'), slot) |
515 | + self.assertEqual([], self.logger.log) |
516 | + emitter.trigger_foo() |
517 | + self.assertEqual([('trapping_foo',)], self.logger.log) |
518 | + emitter.trigger_bar(1, 2) # not connected |
519 | + self.assertEqual([('trapping_foo',)], self.logger.log) |
520 | + emitter.trigger_foo() |
521 | + self.assertEqual([('trapping_foo',), ('trapping_foo',)], |
522 | + self.logger.log) |
523 | + slot = self.logger.createNamedSlot('trapping_bar') |
524 | + QtCore.QObject.connect(emitter, QtCore.SIGNAL('bar(int, int)'), slot) |
525 | + emitter.trigger_bar(1, 2) |
526 | + self.assertEqual([('trapping_foo',), ('trapping_foo',), |
527 | + ('trapping_bar', 1, 2), |
528 | + ], self.logger.log) |
529 | + |
530 | + def test_connectToNamedSlot(self): |
531 | + emitter = _Emitter() |
532 | + self.logger.connectToNamedSlot('foo()', emitter) |
533 | + self.logger.connectToNamedSlot('bar(int, int)', emitter) |
534 | + emitter.trigger_foo() |
535 | + self.assertEqual([('foo()',)], self.logger.log) |
536 | + emitter.trigger_bar(3, 4) |
537 | + self.assertEqual([('foo()',), ('bar(int, int)', 3, 4)], |
538 | + self.logger.log) |
539 | + |
540 | |
541 | === added file 'tests/test_welcome.py' |
542 | --- tests/test_welcome.py 1970-01-01 00:00:00 +0000 |
543 | +++ tests/test_welcome.py 2009-07-08 19:40:55 +0000 |
544 | @@ -0,0 +1,27 @@ |
545 | +# Copyright (C) 2009 Canonical Ltd |
546 | +# |
547 | +# This program is free software; you can redistribute it and/or modify |
548 | +# it under the terms of the GNU General Public License as published by |
549 | +# the Free Software Foundation; either version 2 of the License, or |
550 | +# (at your option) any later version. |
551 | +# |
552 | +# This program is distributed in the hope that it will be useful, |
553 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
554 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
555 | +# GNU General Public License for more details. |
556 | +# |
557 | +# You should have received a copy of the GNU General Public License |
558 | +# along with this program; if not, write to the Free Software |
559 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
560 | + |
561 | +"""Tests for the WelcomeModel""" |
562 | + |
563 | +from bzrlib.plugins.explorer import tests |
564 | +from bzrlib.plugins.explorer.lib import welcome |
565 | + |
566 | + |
567 | +class TestWelcomeModel(tests.TestCaseInTempDir): |
568 | + |
569 | + def test__init__(self): |
570 | + model = welcome.WelcomeModel('a url', None, None, None) |
571 | + |
572 | |
573 | === added file 'tests/test_wt_model.py' |
574 | --- tests/test_wt_model.py 1970-01-01 00:00:00 +0000 |
575 | +++ tests/test_wt_model.py 2009-07-09 19:46:58 +0000 |
576 | @@ -0,0 +1,325 @@ |
577 | +# Copyright (C) 2009 Canonical Ltd |
578 | +# |
579 | +# This program is free software; you can redistribute it and/or modify |
580 | +# it under the terms of the GNU General Public License as published by |
581 | +# the Free Software Foundation; either version 2 of the License, or |
582 | +# (at your option) any later version. |
583 | +# |
584 | +# This program is distributed in the hope that it will be useful, |
585 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
586 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
587 | +# GNU General Public License for more details. |
588 | +# |
589 | +# You should have received a copy of the GNU General Public License |
590 | +# along with this program; if not, write to the Free Software |
591 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
592 | + |
593 | +"""tests for the WorkingTreeModel class""" |
594 | + |
595 | +from PyQt4 import QtCore, QtGui |
596 | + |
597 | +from bzrlib import ( |
598 | + errors, |
599 | + inventory, |
600 | + tests, |
601 | + ) |
602 | + |
603 | +from bzrlib.plugins.explorer.lib import wt_model |
604 | +from bzrlib.plugins.explorer.tests import TestCaseWithQt |
605 | + |
606 | + |
607 | +class TestWTModel(TestCaseWithQt): |
608 | + |
609 | + def test_flags_empty(self): |
610 | + wt = self.make_branch_and_tree('.') |
611 | + model = wt_model.WorkingTreeModel(wt) |
612 | + self.assertEqual(0, model.flags(QtCore.QModelIndex())) |
613 | + |
614 | + def test_flags_are_readonly(self): |
615 | + wt = self.make_branch_and_tree('.') |
616 | + self.build_tree(['a_file']) |
617 | + model = wt_model.WorkingTreeModel(wt) |
618 | + index = model.index(0, 0, None) |
619 | + self.assertEqual(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable, |
620 | + model.flags(index)) |
621 | + |
622 | + def test_data_invalid(self): |
623 | + wt = self.make_branch_and_tree('.') |
624 | + self.build_tree(['a_file']) |
625 | + model = wt_model.WorkingTreeModel(wt) |
626 | + res = model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole) |
627 | + self.assertIsInstance(res, QtCore.QVariant) |
628 | + self.assertFalse(res.isValid()) |
629 | + index = model.index(0, 0, None) |
630 | + # TODO: We probably could implement ToolTipRole, perhaps returning the |
631 | + # full path? |
632 | + res = model.data(QtCore.QModelIndex(), QtCore.Qt.ToolTipRole) |
633 | + self.assertIsInstance(res, QtCore.QVariant) |
634 | + self.assertFalse(res.isValid()) |
635 | + |
636 | + def test_data(self): |
637 | + wt = self.make_branch_and_tree('.') |
638 | + self.build_tree(['dir/', 'dir/a_file', 'b_file']) |
639 | + wt.add(['dir', 'dir/a_file', 'b_file'], |
640 | + ['dir-id', 'a-id', 'b-id']) |
641 | + model = wt_model.WorkingTreeModel(wt) |
642 | + root_index = model.index(0, 0, None) |
643 | + index = model.index(1, 0, root_index) |
644 | + res = model.data(index, QtCore.Qt.DisplayRole) |
645 | + self.assertIsInstance(res, QtCore.QVariant) |
646 | + self.assertEqual('dir', res.toString()) |
647 | + index = model.index(1, 2, root_index) |
648 | + res = model.data(index, QtCore.Qt.DisplayRole) |
649 | + self.assertIsInstance(res, QtCore.QVariant) |
650 | + # New file, just added, no executable |
651 | + self.assertEqual('N+ ', res.toString()) |
652 | + a_index = model.index(0, 0, index) |
653 | + res = model.data(a_index, QtCore.Qt.DisplayRole) |
654 | + self.assertIsInstance(res, QtCore.QVariant) |
655 | + self.assertEqual('a_file', res.toString()) |
656 | + a_index = model.index(0, 3, index) |
657 | + res = model.data(a_index, QtCore.Qt.DisplayRole) |
658 | + self.assertIsInstance(res, QtCore.QVariant) |
659 | + self.assertEqual('dir/a_file', res.toString()) |
660 | + |
661 | + def test_data_decoraterole(self): |
662 | + wt = self.make_branch_and_tree('.') |
663 | + self.build_tree(['a_file.txt']) |
664 | + wt.add(['a_file.txt'], ['a-id']) |
665 | + model = wt_model.WorkingTreeModel(wt) |
666 | + root_index = model.index(0, 0, None) |
667 | + a_index = model.index(0, 0, root_index) |
668 | + self.assertEqual('a_file.txt', a_index.internalPointer().name) |
669 | + res = model.data(a_index, QtCore.Qt.DecorationRole) |
670 | + self.assertIsInstance(res, QtCore.QVariant) |
671 | + self.assertTrue(res.isValid()) |
672 | + self.assertEqual('QIcon', res.typeName()) |
673 | + # For records not at column 0, we should get nothing |
674 | + a_index = model.index(0, 1, root_index) |
675 | + res = model.data(a_index, QtCore.Qt.DecorationRole) |
676 | + self.assertIsInstance(res, QtCore.QVariant) |
677 | + self.assertFalse(res.isValid()) |
678 | + |
679 | + def test_index_invalid(self): |
680 | + wt = self.make_branch_and_tree('.') |
681 | + model = wt_model.WorkingTreeModel(wt) |
682 | + res = model.index(100, 100, None) |
683 | + self.assertIsInstance(res, QtCore.QModelIndex) |
684 | + self.assertFalse(res.isValid()) |
685 | + |
686 | + def test_index_empty(self): |
687 | + wt = self.make_branch_and_tree('.') |
688 | + model = wt_model.WorkingTreeModel(wt) |
689 | + res = model.index(0, 0, None) |
690 | + self.assertIsInstance(res, QtCore.QModelIndex) |
691 | + self.assertTrue(res.isValid()) |
692 | + |
693 | + def test_index_one_item(self): |
694 | + wt = self.make_branch_and_tree('.') |
695 | + self.build_tree(['a_file']) |
696 | + model = wt_model.WorkingTreeModel(wt) |
697 | + res = model.index(0, 0, None) |
698 | + self.assertIsInstance(res, QtCore.QModelIndex) |
699 | + self.assertTrue(res.isValid()) |
700 | + self.assertEqual('', res.internalPointer().name) |
701 | + |
702 | + def test_index_subdir(self): |
703 | + wt = self.make_branch_and_tree('.') |
704 | + self.build_tree(['dir/', 'dir/a_file', 'b_file']) |
705 | + wt.add(['dir', 'dir/a_file', 'b_file'], |
706 | + ['dir-id', 'a-id', 'b-id']) |
707 | + model = wt_model.WorkingTreeModel(wt) |
708 | + root_index = model.index(0, 0, None) |
709 | + self.assertIsInstance(root_index, QtCore.QModelIndex) |
710 | + self.assertTrue(root_index.isValid()) |
711 | + res = model.index(1, 0, root_index) |
712 | + self.assertEqual('dir', res.internalPointer().name) |
713 | + res = model.index(0, 0, res) |
714 | + self.assertIsInstance(res, QtCore.QModelIndex) |
715 | + self.assertTrue(res.isValid()) |
716 | + self.assertEqual('a_file', res.internalPointer().name) |
717 | + |
718 | + def test_rowCount_simple(self): |
719 | + wt = self.make_branch_and_tree('.') |
720 | + model = wt_model.WorkingTreeModel(wt) |
721 | + self.assertEqual(1, model.rowCount()) |
722 | + |
723 | + def test_rowCount_subdirs(self): |
724 | + wt = self.make_branch_and_tree('.') |
725 | + self.build_tree(['dir/', 'dir/a_file', 'b_file']) |
726 | + wt.add(['dir', 'dir/a_file', 'b_file'], |
727 | + ['dir-id', 'a-id', 'b-id']) |
728 | + model = wt_model.WorkingTreeModel(wt) |
729 | + self.assertEqual(1, model.rowCount()) |
730 | + root_index = model.index(0, 0, None) |
731 | + self.assertIsInstance(root_index, QtCore.QModelIndex) |
732 | + self.assertTrue(root_index.isValid()) |
733 | + self.assertEqual('', root_index.internalPointer().name) |
734 | + self.assertEqual(2, model.rowCount(root_index)) |
735 | + |
736 | + def test_parent_empty(self): |
737 | + wt = self.make_branch_and_tree('.') |
738 | + model = wt_model.WorkingTreeModel(wt) |
739 | + res = model.parent(QtCore.QModelIndex()) |
740 | + self.assertIsInstance(res, QtCore.QModelIndex) |
741 | + self.assertFalse(res.isValid()) |
742 | + res = model.parent(None) |
743 | + self.assertIsInstance(res, QtCore.QModelIndex) |
744 | + self.assertFalse(res.isValid()) |
745 | + |
746 | + def test_parent_with_subdirs(self): |
747 | + wt = self.make_branch_and_tree('.') |
748 | + self.build_tree(['dir/', 'dir/a_file', 'b_file']) |
749 | + wt.add(['dir', 'dir/a_file', 'b_file'], |
750 | + ['dir-id', 'a-id', 'b-id']) |
751 | + model = wt_model.WorkingTreeModel(wt) |
752 | + self.assertEqual(1, model.rowCount()) |
753 | + root_index = model.index(0, 0, None) |
754 | + self.assertIsInstance(root_index, QtCore.QModelIndex) |
755 | + self.assertTrue(root_index.isValid()) |
756 | + self.assertEqual('', root_index.internalPointer().name) |
757 | + res = model.parent(root_index) |
758 | + self.assertIsInstance(res, QtCore.QModelIndex) |
759 | + self.assertFalse(res.isValid()) |
760 | + dir_index = model.index(1, 0, root_index) |
761 | + self.assertIsInstance(dir_index, QtCore.QModelIndex) |
762 | + self.assertTrue(dir_index.isValid()) |
763 | + self.assertEqual('dir', dir_index.internalPointer().name) |
764 | + res = model.parent(dir_index) |
765 | + self.assertIsInstance(res, QtCore.QModelIndex) |
766 | + self.assertTrue(res.isValid()) |
767 | + self.assertEqual('', res.internalPointer().name) |
768 | + |
769 | + def test_headerData(self): |
770 | + wt = self.make_branch_and_tree('.') |
771 | + model = wt_model.WorkingTreeModel(wt) |
772 | + self.assertEqual(len(model._headers), model.columnCount()) |
773 | + res = model.headerData(0, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole) |
774 | + self.assertIsInstance(res, QtCore.QVariant) |
775 | + self.assertEqual('name', res.toString()) |
776 | + res = model.headerData(1, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole) |
777 | + self.assertEqual('kind', res.toString()) |
778 | + res = model.headerData(2, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole) |
779 | + self.assertEqual('status', res.toString()) |
780 | + res = model.headerData(3, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole) |
781 | + self.assertEqual('path', res.toString()) |
782 | + |
783 | + res = model.headerData(len(model._headers), |
784 | + QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole) |
785 | + self.assertIsInstance(res, QtCore.QVariant) |
786 | + self.assertTrue(res.isNull()) |
787 | + res = model.headerData(0, QtCore.Qt.Vertical, QtCore.Qt.DisplayRole) |
788 | + self.assertIsInstance(res, QtCore.QVariant) |
789 | + self.assertTrue(res.isNull()) |
790 | + res = model.headerData(0, QtCore.Qt.Vertical, QtCore.Qt.ToolTipRole) |
791 | + self.assertIsInstance(res, QtCore.QVariant) |
792 | + self.assertTrue(res.isNull()) |
793 | + |
794 | + |
795 | +class Test_WTItem(tests.TestCaseWithTransport): |
796 | + |
797 | + def test_from_wt_simple(self): |
798 | + wt = self.make_branch_and_tree('.') |
799 | + self.build_tree(['a_file']) |
800 | + wt.add(['a_file'], ['file-id']) |
801 | + wt.set_root_id('root-id') |
802 | + root_item = wt_model._WTItem.create_from_wt(wt) |
803 | + self.assertIsInstance(root_item, wt_model._WTItem) |
804 | + self.assertIs(None, root_item.path) |
805 | + self.assertEqual(1, len(root_item.children)) |
806 | + tree_root_item = root_item.children[0] |
807 | + self.assertEqual('', tree_root_item.path) |
808 | + self.assertEqual('', tree_root_item.name) |
809 | + self.assertEqual('directory', tree_root_item.kind) |
810 | + self.assertEqual('root-id', tree_root_item.file_id) |
811 | + self.assertIs(None, tree_root_item.old_kind) |
812 | + self.assertIs(root_item, tree_root_item.parent) |
813 | + self.assertEqual(1, len(tree_root_item.children)) |
814 | + child = tree_root_item.children[0] |
815 | + self.assertEqual('a_file', child.path) |
816 | + self.assertEqual('a_file', child.name) |
817 | + self.assertEqual('file', child.kind) |
818 | + self.assertEqual('file-id', child.file_id) |
819 | + self.assertIs(None, child.old_kind) |
820 | + self.assertEqual(tree_root_item, child.parent) |
821 | + self.assertEqual(0, child.row()) |
822 | + |
823 | + def test_unversioned_from_wt(self): |
824 | + wt = self.make_branch_and_tree('.') |
825 | + self.build_tree(['a_file', 'unversioned-file']) |
826 | + wt.set_root_id('root-id') |
827 | + wt.add(['a_file'], ['file-id']) |
828 | + root_item = wt_model._WTItem.create_from_wt(wt) |
829 | + self.assertIsInstance(root_item, wt_model._WTItem) |
830 | + self.assertIs(None, root_item.path) |
831 | + self.assertEqual(1, len(root_item.children)) |
832 | + tree_root_item = root_item.children[0] |
833 | + self.assertIsInstance(tree_root_item, wt_model._WTItem) |
834 | + self.assertEqual('', tree_root_item.path) |
835 | + self.assertEqual('', tree_root_item.name) |
836 | + self.assertEqual('directory', tree_root_item.kind) |
837 | + self.assertEqual('root-id', tree_root_item.file_id) |
838 | + self.assertIs(None, tree_root_item.old_kind) |
839 | + self.assertIs(root_item, tree_root_item.parent) |
840 | + self.assertEqual(2, len(tree_root_item.children)) |
841 | + child = tree_root_item.children[0] |
842 | + self.assertEqual('a_file', child.path) |
843 | + self.assertEqual('a_file', child.name) |
844 | + self.assertEqual('file', child.kind) |
845 | + self.assertEqual('file-id', child.file_id) |
846 | + self.assertIs(None, child.old_kind) |
847 | + self.assertEqual(tree_root_item, child.parent) |
848 | + self.assertEqual(0, child.row()) |
849 | + child = tree_root_item.children[1] |
850 | + self.assertEqual('unversioned-file', child.path) |
851 | + self.assertEqual('unversioned-file', child.name) |
852 | + self.assertEqual('file', child.kind) |
853 | + self.assertIs(None, child.file_id) |
854 | + self.assertIs(None, child.old_kind) |
855 | + self.assertEqual(tree_root_item, child.parent) |
856 | + self.assertEqual(1, child.row()) |
857 | + |
858 | + def test_with_subdirs(self): |
859 | + wt = self.make_branch_and_tree('.') |
860 | + self.build_tree(['dir/', 'dir/subdir/', |
861 | + 'dir/subdir/a_file']) |
862 | + wt.set_root_id('root-id') |
863 | + wt.add(['dir', 'dir/subdir', 'dir/subdir/a_file'], |
864 | + ['dir-id', 'subdir-id', 'file-id']) |
865 | + root_item = wt_model._WTItem.create_from_wt(wt) |
866 | + self.assertIsInstance(root_item, wt_model._WTItem) |
867 | + self.assertIs(None, root_item.path) |
868 | + self.assertEqual(1, len(root_item.children)) |
869 | + tree_root_item = root_item.children[0] |
870 | + self.assertIsInstance(tree_root_item, wt_model._WTItem) |
871 | + self.assertEqual('root-id', tree_root_item.file_id) |
872 | + self.assertIs(root_item, tree_root_item.parent) |
873 | + self.assertEqual(1, len(tree_root_item.children)) |
874 | + child = tree_root_item.children[0] |
875 | + self.assertEqual('dir', child.path) |
876 | + self.assertEqual('dir', child.name) |
877 | + self.assertEqual('directory', child.kind) |
878 | + self.assertEqual('dir-id', child.file_id) |
879 | + self.assertIs(None, child.old_kind) |
880 | + self.assertEqual(tree_root_item, child.parent) |
881 | + self.assertEqual(0, tree_root_item.row()) |
882 | + self.assertEqual(1, len(child.children)) |
883 | + this_dir = child |
884 | + child = this_dir.children[0] |
885 | + self.assertEqual('dir/subdir', child.path) |
886 | + self.assertEqual('subdir', child.name) |
887 | + self.assertEqual('directory', child.kind) |
888 | + self.assertEqual('subdir-id', child.file_id) |
889 | + self.assertIs(None, child.old_kind) |
890 | + self.assertEqual(this_dir, child.parent) |
891 | + self.assertEqual(0, child.row()) |
892 | + self.assertEqual(1, len(child.children)) |
893 | + this_dir = child |
894 | + child = this_dir.children[0] |
895 | + self.assertEqual('dir/subdir/a_file', child.path) |
896 | + self.assertEqual('a_file', child.name) |
897 | + self.assertEqual('file', child.kind) |
898 | + self.assertEqual('file-id', child.file_id) |
899 | + self.assertIs(None, child.old_kind) |
900 | + self.assertEqual(this_dir, child.parent) |
901 | + self.assertEqual(0, child.row()) |
I don't think this is 100% ready for merging, but I figured I should make sure other people were aware that I was working on it.
This patch builds on my earlier "add a test suite" patches, so hopefully that noise doesn't bother anyone (since those still haven't landed yet).
Anyway, this add as WorkingTreeModel class. It could be arguably called an "IterChangesModel" because it wraps the result of iter_changes() into a QtAbstractItemModel class.
That basically allows you to see the versioned status, etc. It uses want_unversione d=True so that it gives the status of all files (if only because you need a directory to be able to put children underneath it.)
Some of the motivation was wanting to be able to handle: /bugs.edge. launchpad. net/bzr- explorer/ +bug/396662
https:/
(working tree should de-emphasize ignored files)
I was also wondering if something like this would be better placed into QBzr, since they likely need it there as well.