Merge lp:~mvo/software-center/treeview-keep-state-on-db-cache-change into lp:software-center
- treeview-keep-state-on-db-cache-change
- Merge into trunk
Proposed by
Michael Vogt
Status: | Merged |
---|---|
Merged at revision: | 2933 |
Proposed branch: | lp:~mvo/software-center/treeview-keep-state-on-db-cache-change |
Merge into: | lp:software-center |
Diff against target: |
283 lines (+84/-28) 5 files modified
softwarecenter/ui/gtk3/models/appstore2.py (+10/-1) softwarecenter/ui/gtk3/panes/installedpane.py (+54/-6) softwarecenter/ui/gtk3/panes/softwarepane.py (+0/-9) softwarecenter/ui/gtk3/views/appview.py (+12/-2) softwarecenter/ui/gtk3/widgets/apptreeview.py (+8/-10) |
To merge this branch: | bzr merge lp:~mvo/software-center/treeview-keep-state-on-db-cache-change |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Lasker (community) | Approve | ||
Review via email: mp+99946@code.launchpad.net |
Commit message
Description of the change
This branch depends on the other branches:
lp:~mvo/software-center/lp964433
lp:~mvo/software-center/lp966879
lp:~mvo/software-center/lp846204
and adds the ability to save/restore the state of the installedpane treeview when the db or the cache changes.
This means that if you e.g. uninstal a application when the view refreshes the expanded nodes and scrollposition
is saved.
To test, go to the installed pane, expand some rows and run:
$ dbus-send --print-reply --dest=
or simply remove a application there.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'softwarecenter/ui/gtk3/models/appstore2.py' | |||
2 | --- softwarecenter/ui/gtk3/models/appstore2.py 2012-03-16 10:44:05 +0000 | |||
3 | +++ softwarecenter/ui/gtk3/models/appstore2.py 2012-03-29 14:01:25 +0000 | |||
4 | @@ -61,6 +61,9 @@ | |||
5 | 61 | self.pkg_count = pkg_count | 61 | self.pkg_count = pkg_count |
6 | 62 | self.vis_count = pkg_count | 62 | self.vis_count = pkg_count |
7 | 63 | 63 | ||
8 | 64 | def __repr__(self): | ||
9 | 65 | return "[CategoryRowReference: name=%s]" % self.untranslated_name | ||
10 | 66 | |||
11 | 64 | 67 | ||
12 | 65 | class UncategorisedRowRef(CategoryRowReference): | 68 | class UncategorisedRowRef(CategoryRowReference): |
13 | 66 | 69 | ||
14 | @@ -75,6 +78,9 @@ | |||
15 | 75 | display_name, | 78 | display_name, |
16 | 76 | None, pkg_count) | 79 | None, pkg_count) |
17 | 77 | 80 | ||
18 | 81 | def __repr__(self): | ||
19 | 82 | return "[UncategorizedRowReference: name=%s]" % self.untranslated_name | ||
20 | 83 | |||
21 | 78 | 84 | ||
22 | 79 | class AppPropertiesHelper(GObject.GObject): | 85 | class AppPropertiesHelper(GObject.GObject): |
23 | 80 | """ Baseclass that contains common functions for our | 86 | """ Baseclass that contains common functions for our |
24 | @@ -409,6 +415,7 @@ | |||
25 | 409 | """ set the content of the liststore based on a list of | 415 | """ set the content of the liststore based on a list of |
26 | 410 | xapian.MSetItems | 416 | xapian.MSetItems |
27 | 411 | """ | 417 | """ |
28 | 418 | LOG.debug("set_from_matches len(matches)='%s'" % len(matches)) | ||
29 | 412 | self.current_matches = matches | 419 | self.current_matches = matches |
30 | 413 | n_matches = len(matches) | 420 | n_matches = len(matches) |
31 | 414 | if n_matches == 0: | 421 | if n_matches == 0: |
32 | @@ -432,6 +439,7 @@ | |||
33 | 432 | self.buffer_icons() | 439 | self.buffer_icons() |
34 | 433 | 440 | ||
35 | 434 | def load_range(self, indices, step): | 441 | def load_range(self, indices, step): |
36 | 442 | LOG.debug("load_range: %s %s" % (indices, step)) | ||
37 | 435 | db = self.db.xapiandb | 443 | db = self.db.xapiandb |
38 | 436 | matches = self.current_matches | 444 | matches = self.current_matches |
39 | 437 | 445 | ||
40 | @@ -446,7 +454,8 @@ | |||
41 | 446 | for i in range(start, end): | 454 | for i in range(start, end): |
42 | 447 | try: | 455 | try: |
43 | 448 | row_content = self[(i,)][0] | 456 | row_content = self[(i,)][0] |
45 | 449 | except IndexError: | 457 | except IndexError as e: |
46 | 458 | LOG.warn("failed to load rows: '%s'" % e) | ||
47 | 450 | break | 459 | break |
48 | 451 | 460 | ||
49 | 452 | if row_content: | 461 | if row_content: |
50 | 453 | 462 | ||
51 | === modified file 'softwarecenter/ui/gtk3/panes/installedpane.py' | |||
52 | --- softwarecenter/ui/gtk3/panes/installedpane.py 2012-03-27 22:55:49 +0000 | |||
53 | +++ softwarecenter/ui/gtk3/panes/installedpane.py 2012-03-29 14:01:25 +0000 | |||
54 | @@ -325,8 +325,25 @@ | |||
55 | 325 | self.nonapps_visible = NonAppVisibility.NEVER_VISIBLE | 325 | self.nonapps_visible = NonAppVisibility.NEVER_VISIBLE |
56 | 326 | self.refresh_apps() | 326 | self.refresh_apps() |
57 | 327 | 327 | ||
58 | 328 | def _save_treeview_state(self): | ||
59 | 329 | # store the state | ||
60 | 330 | expanded_rows = [] | ||
61 | 331 | self.app_view.tree_view.map_expanded_rows( | ||
62 | 332 | lambda view, path, data: expanded_rows.append(path.to_string()), | ||
63 | 333 | None) | ||
64 | 334 | vadj = self.app_view.tree_view_scroll.get_vadjustment().get_value() | ||
65 | 335 | return expanded_rows, vadj | ||
66 | 336 | |||
67 | 337 | def _restore_treeview_state(self, state): | ||
68 | 338 | expanded_rows, vadj = state | ||
69 | 339 | for ind in expanded_rows: | ||
70 | 340 | path = Gtk.TreePath.new_from_string(ind) | ||
71 | 341 | self.app_view.tree_view.expand_row(path, False) | ||
72 | 342 | self.app_view.tree_view_scroll.get_vadjustment().set_lower(vadj) | ||
73 | 343 | self.app_view.tree_view_scroll.get_vadjustment().set_value(vadj) | ||
74 | 344 | |||
75 | 328 | #~ @interrupt_build_and_wait | 345 | #~ @interrupt_build_and_wait |
77 | 329 | def _build_categorised_installedview(self): | 346 | def _build_categorised_installedview(self, keep_state=False): |
78 | 330 | LOG.debug('Rebuilding categorised installedview...') | 347 | LOG.debug('Rebuilding categorised installedview...') |
79 | 331 | 348 | ||
80 | 332 | # display the busy cursor and a local spinner while we build the view | 349 | # display the busy cursor and a local spinner while we build the view |
81 | @@ -335,6 +352,13 @@ | |||
82 | 335 | window.set_cursor(self.busy_cursor) | 352 | window.set_cursor(self.busy_cursor) |
83 | 336 | self.show_installed_view_spinner() | 353 | self.show_installed_view_spinner() |
84 | 337 | 354 | ||
85 | 355 | if keep_state: | ||
86 | 356 | treeview_state = self._save_treeview_state() | ||
87 | 357 | |||
88 | 358 | # disconnect the model to avoid e.g. updates of "cursor-changed" | ||
89 | 359 | # AppTreeView.expand_path while the model is in rebuild-flux | ||
90 | 360 | self.app_view.set_model(None) | ||
91 | 361 | |||
92 | 338 | model = self.base_model # base model not treefilter | 362 | model = self.base_model # base model not treefilter |
93 | 339 | model.clear() | 363 | model.clear() |
94 | 340 | 364 | ||
95 | @@ -410,6 +434,10 @@ | |||
96 | 410 | self.app_view._append_appcount(self.installed_count, | 434 | self.app_view._append_appcount(self.installed_count, |
97 | 411 | mode=AppView.INSTALLED_MODE) | 435 | mode=AppView.INSTALLED_MODE) |
98 | 412 | 436 | ||
99 | 437 | self.app_view.set_model(self.treefilter) | ||
100 | 438 | if keep_state: | ||
101 | 439 | self._restore_treeview_state(treeview_state) | ||
102 | 440 | |||
103 | 413 | # hide the local spinner | 441 | # hide the local spinner |
104 | 414 | self.hide_installed_view_spinner() | 442 | self.hide_installed_view_spinner() |
105 | 415 | 443 | ||
106 | @@ -425,7 +453,7 @@ | |||
107 | 425 | 453 | ||
108 | 426 | GObject.idle_add(profiled_rebuild_categorised_view) | 454 | GObject.idle_add(profiled_rebuild_categorised_view) |
109 | 427 | 455 | ||
111 | 428 | def _build_oneconfview(self): | 456 | def _build_oneconfview(self, keep_state=False): |
112 | 429 | LOG.debug('Rebuilding oneconfview for %s...' % self.current_hostid) | 457 | LOG.debug('Rebuilding oneconfview for %s...' % self.current_hostid) |
113 | 430 | 458 | ||
114 | 431 | # display the busy cursor and the local spinner while we build the view | 459 | # display the busy cursor and the local spinner while we build the view |
115 | @@ -434,6 +462,13 @@ | |||
116 | 434 | window.set_cursor(self.busy_cursor) | 462 | window.set_cursor(self.busy_cursor) |
117 | 435 | self.show_installed_view_spinner() | 463 | self.show_installed_view_spinner() |
118 | 436 | 464 | ||
119 | 465 | if keep_state: | ||
120 | 466 | treeview_state = self._save_treeview_state() | ||
121 | 467 | |||
122 | 468 | # disconnect the model to avoid e.g. updates of "cursor-changed" | ||
123 | 469 | # AppTreeView.expand_path while the model is in rebuild-flux | ||
124 | 470 | self.app_view.set_model(None) | ||
125 | 471 | |||
126 | 437 | model = self.base_model # base model not treefilter | 472 | model = self.base_model # base model not treefilter |
127 | 438 | model.clear() | 473 | model.clear() |
128 | 439 | 474 | ||
129 | @@ -518,6 +553,10 @@ | |||
130 | 518 | self.app_view._append_appcount(self.installed_count, | 553 | self.app_view._append_appcount(self.installed_count, |
131 | 519 | mode=AppView.DIFF_MODE) | 554 | mode=AppView.DIFF_MODE) |
132 | 520 | 555 | ||
133 | 556 | self.app_view.set_model(self.treefilter) | ||
134 | 557 | if keep_state: | ||
135 | 558 | self._restore_treeview_state(treeview_state) | ||
136 | 559 | |||
137 | 521 | # hide the local spinner | 560 | # hide the local spinner |
138 | 522 | self.hide_installed_view_spinner() | 561 | self.hide_installed_view_spinner() |
139 | 523 | 562 | ||
140 | @@ -599,10 +638,11 @@ | |||
141 | 599 | def refresh_apps(self, *args, **kwargs): | 638 | def refresh_apps(self, *args, **kwargs): |
142 | 600 | """refresh the applist and update the navigation bar """ | 639 | """refresh the applist and update the navigation bar """ |
143 | 601 | logging.debug("installedpane refresh_apps") | 640 | logging.debug("installedpane refresh_apps") |
144 | 641 | keep_state = kwargs.get("keep_state", False) | ||
145 | 602 | if self.current_hostid: | 642 | if self.current_hostid: |
147 | 603 | self._build_oneconfview() | 643 | self._build_oneconfview(keep_state) |
148 | 604 | else: | 644 | else: |
150 | 605 | self._build_categorised_installedview() | 645 | self._build_categorised_installedview(keep_state) |
151 | 606 | 646 | ||
152 | 607 | def _clear_search(self): | 647 | def _clear_search(self): |
153 | 608 | # remove the details and clear the search | 648 | # remove the details and clear the search |
154 | @@ -629,9 +669,17 @@ | |||
155 | 629 | self.app_view._append_appcount(appcount, mode=AppView.DIFF_MODE) | 669 | self.app_view._append_appcount(appcount, mode=AppView.DIFF_MODE) |
156 | 630 | return vis_cats | 670 | return vis_cats |
157 | 631 | 671 | ||
158 | 672 | def _refresh_on_cache_or_db_change(self): | ||
159 | 673 | self.refresh_apps(keep_state=True) | ||
160 | 674 | self.app_details_view.refresh_app() | ||
161 | 675 | |||
162 | 632 | def on_db_reopen(self, db): | 676 | def on_db_reopen(self, db): |
165 | 633 | self.refresh_apps(rebuild=True) | 677 | LOG.debug("on_db_reopen") |
166 | 634 | self.app_details_view.refresh_app() | 678 | self._refresh_on_cache_or_db_change() |
167 | 679 | |||
168 | 680 | def on_cache_ready(self, cache): | ||
169 | 681 | LOG.debug("on_cache_ready") | ||
170 | 682 | self._refresh_on_cache_or_db_change() | ||
171 | 635 | 683 | ||
172 | 636 | def on_application_selected(self, appview, app): | 684 | def on_application_selected(self, appview, app): |
173 | 637 | """callback when an app is selected""" | 685 | """callback when an app is selected""" |
174 | 638 | 686 | ||
175 | === modified file 'softwarecenter/ui/gtk3/panes/softwarepane.py' | |||
176 | --- softwarecenter/ui/gtk3/panes/softwarepane.py 2012-03-20 08:23:24 +0000 | |||
177 | +++ softwarecenter/ui/gtk3/panes/softwarepane.py 2012-03-29 14:01:25 +0000 | |||
178 | @@ -254,15 +254,6 @@ | |||
179 | 254 | def on_cache_ready(self, cache): | 254 | def on_cache_ready(self, cache): |
180 | 255 | " refresh the application list when the cache is re-opened " | 255 | " refresh the application list when the cache is re-opened " |
181 | 256 | LOG.debug("on_cache_ready") | 256 | LOG.debug("on_cache_ready") |
182 | 257 | # it only makes sense to refresh if there is something to | ||
183 | 258 | # refresh, otherwise we create a bunch of (not yet needed) | ||
184 | 259 | # AppStore objects on startup when the cache sends its | ||
185 | 260 | # initial "cache-ready" signal | ||
186 | 261 | model = self.app_view.tree_view.get_model() | ||
187 | 262 | if model is None: | ||
188 | 263 | return | ||
189 | 264 | # FIXME: preserve selection too | ||
190 | 265 | self.refresh_apps() | ||
191 | 266 | 257 | ||
192 | 267 | @wait_for_apt_cache_ready | 258 | @wait_for_apt_cache_ready |
193 | 268 | def on_application_activated(self, appview, app): | 259 | def on_application_activated(self, appview, app): |
194 | 269 | 260 | ||
195 | === modified file 'softwarecenter/ui/gtk3/views/appview.py' | |||
196 | --- softwarecenter/ui/gtk3/views/appview.py 2012-03-14 16:34:56 +0000 | |||
197 | +++ softwarecenter/ui/gtk3/views/appview.py 2012-03-29 14:01:25 +0000 | |||
198 | @@ -176,13 +176,16 @@ | |||
199 | 176 | def set_model(self, model): | 176 | def set_model(self, model): |
200 | 177 | self.tree_view.set_model(model) | 177 | self.tree_view.set_model(model) |
201 | 178 | 178 | ||
202 | 179 | def get_model(self): | ||
203 | 180 | return self.tree_view.appmodel | ||
204 | 181 | |||
205 | 179 | def display_matches(self, matches, is_search=False): | 182 | def display_matches(self, matches, is_search=False): |
206 | 180 | # FIXME: installedpane handles display of the trees intimately, | 183 | # FIXME: installedpane handles display of the trees intimately, |
207 | 181 | # so for the time being lets just return None in the case of our | 184 | # so for the time being lets just return None in the case of our |
208 | 182 | # TreeView displaying an AppTreeStore ... ;( | 185 | # TreeView displaying an AppTreeStore ... ;( |
209 | 183 | # ... also we dont currently support user sorting in the | 186 | # ... also we dont currently support user sorting in the |
210 | 184 | # installedview, so issue is somewhat moot for the time being... | 187 | # installedview, so issue is somewhat moot for the time being... |
212 | 185 | if isinstance(self.tree_view.appmodel, AppTreeStore): | 188 | if isinstance(self.get_model(), AppTreeStore): |
213 | 186 | LOG.debug("display_matches called on AppTreeStore, ignoring") | 189 | LOG.debug("display_matches called on AppTreeStore, ignoring") |
214 | 187 | return | 190 | return |
215 | 188 | 191 | ||
216 | @@ -197,9 +200,16 @@ | |||
217 | 197 | not self.user_defined_sort_method): | 200 | not self.user_defined_sort_method): |
218 | 198 | self.set_sort_method_with_no_signal(self._SORT_BY_TOP_RATED) | 201 | self.set_sort_method_with_no_signal(self._SORT_BY_TOP_RATED) |
219 | 199 | 202 | ||
221 | 200 | model = self.tree_view.appmodel | 203 | model = self.get_model() |
222 | 204 | # disconnect the model from the view before running | ||
223 | 205 | # set_from_matches to ensure that the _cell_data_func_cb is not | ||
224 | 206 | # run when the placeholder items are set, otherwise the purpose | ||
225 | 207 | # of the "load-on-demand" is gone and it leads to bugs like | ||
226 | 208 | # LP: #964433 | ||
227 | 209 | self.set_model(None) | ||
228 | 201 | if model: | 210 | if model: |
229 | 202 | model.set_from_matches(matches) | 211 | model.set_from_matches(matches) |
230 | 212 | self.set_model(model) | ||
231 | 203 | self.user_defined_sort_method = False | 213 | self.user_defined_sort_method = False |
232 | 204 | 214 | ||
233 | 205 | self.tree_view_scroll.get_vadjustment().set_lower(self.vadj) | 215 | self.tree_view_scroll.get_vadjustment().set_lower(self.vadj) |
234 | 206 | 216 | ||
235 | === modified file 'softwarecenter/ui/gtk3/widgets/apptreeview.py' | |||
236 | --- softwarecenter/ui/gtk3/widgets/apptreeview.py 2012-03-22 23:32:06 +0000 | |||
237 | +++ softwarecenter/ui/gtk3/widgets/apptreeview.py 2012-03-29 14:01:25 +0000 | |||
238 | @@ -20,6 +20,9 @@ | |||
239 | 20 | AppGenericStore, CategoryRowReference) | 20 | AppGenericStore, CategoryRowReference) |
240 | 21 | 21 | ||
241 | 22 | 22 | ||
242 | 23 | LOG = logging.getLogger(__name__) | ||
243 | 24 | |||
244 | 25 | |||
245 | 23 | class AppTreeView(Gtk.TreeView): | 26 | class AppTreeView(Gtk.TreeView): |
246 | 24 | 27 | ||
247 | 25 | """Treeview based view component that takes a AppStore and displays it""" | 28 | """Treeview based view component that takes a AppStore and displays it""" |
248 | @@ -144,16 +147,16 @@ | |||
249 | 144 | 147 | ||
250 | 145 | if old is not None: | 148 | if old is not None: |
251 | 146 | start, end = self.get_visible_range() or (None, None) | 149 | start, end = self.get_visible_range() or (None, None) |
254 | 147 | if (start and start.compare(old) != -1) or \ | 150 | if ((start and start.compare(old) != -1) or |
255 | 148 | (end and end.compare(old) != 1): | 151 | (end and end.compare(old) != 1)): |
256 | 149 | self._needs_collapse.append(old) | 152 | self._needs_collapse.append(old) |
257 | 150 | else: | 153 | else: |
258 | 151 | try: # try... a lazy solution to Bug #846204 | 154 | try: # try... a lazy solution to Bug #846204 |
259 | 152 | model.row_changed(old, model.get_iter(old)) | 155 | model.row_changed(old, model.get_iter(old)) |
260 | 153 | except: | 156 | except: |
264 | 154 | msg = ("apptreeview.expand_path: Supplied 'old' " | 157 | LOG.exception( |
265 | 155 | "path is an invalid tree path: '%s'" % old) | 158 | "apptreeview.expand_path: Supplied 'old' " |
266 | 156 | logging.debug(msg) | 159 | "path is an invalid tree path: '%s'" % old) |
267 | 157 | 160 | ||
268 | 158 | if path == None: | 161 | if path == None: |
269 | 159 | return | 162 | return |
270 | @@ -485,13 +488,8 @@ | |||
271 | 485 | path) | 488 | path) |
272 | 486 | 489 | ||
273 | 487 | def _cell_data_func_cb(self, col, cell, model, it, user_data): | 490 | def _cell_data_func_cb(self, col, cell, model, it, user_data): |
274 | 488 | |||
275 | 489 | path = model.get_path(it) | 491 | path = model.get_path(it) |
276 | 490 | 492 | ||
277 | 491 | # this will give us the right underlying model regardless if its | ||
278 | 492 | # a TreeModelFilter, a AppTreeStore or a AppListStore | ||
279 | 493 | model = self.appmodel | ||
280 | 494 | |||
281 | 495 | # this will pre-load data *only* on a AppListStore, it has | 493 | # this will pre-load data *only* on a AppListStore, it has |
282 | 496 | # no effect with a AppTreeStore | 494 | # no effect with a AppTreeStore |
283 | 497 | if model[path][0] is None: | 495 | if model[path][0] is None: |
This is really nice! And definitely a welcome improvement over the complete collapse of the tree after a refresh of the treeview.
At some point we should look at removing the multiple refreshes (and the showing the spinner) that we get on an install/remove.
Thanks, Michael!