More cleanup after review comments.
[appstream:software-center.git] / softwarecenter / ui / gtk3 / views / catview_gtk.py
1 # Copyright (C) 2009 Canonical
2 #
3 # Authors:
4 #  Matthew McGowan
5 #  Michael Vogt
6 #
7 # This program is free software; you can redistribute it and/or modify it under
8 # the terms of the GNU General Public License as published by the Free Software
9 # Foundation; version 3.
10 #
11 # This program is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
14 # details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20 import cairo
21 import gettext
22 from gi.repository import Gtk, GObject
23 import logging
24 import os
25 import xapian
26
27 from gettext import gettext as _
28
29 import softwarecenter.paths
30 from softwarecenter.db.application import Application
31 from softwarecenter.enums import (
32     NonAppVisibility,
33     PkgStates,
34     SortMethods,
35     TOP_RATED_CAROUSEL_LIMIT,
36 )
37 from softwarecenter.utils import wait_for_apt_cache_ready
38 from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper
39 from softwarecenter.ui.gtk3.widgets.viewport import Viewport
40 from softwarecenter.ui.gtk3.widgets.containers import (
41      FramedHeaderBox, FramedBox, FlowableGrid)
42 from softwarecenter.ui.gtk3.widgets.recommendations import (
43                                         RecommendationsPanelLobby,
44                                         RecommendationsPanelCategory)
45 from softwarecenter.ui.gtk3.widgets.exhibits import (
46                                         ExhibitBanner, FeaturedExhibit)
47 from softwarecenter.ui.gtk3.widgets.buttons import (LabelTile,
48                                                     CategoryTile,
49                                                     FeaturedTile)
50 from softwarecenter.ui.gtk3.em import StockEms
51 from softwarecenter.db.appfilter import AppFilter, get_global_filter
52 from softwarecenter.db.enquire import AppEnquire
53 from softwarecenter.db.categories import (Category,
54                                           CategoriesParser,
55                                           get_category_by_name,
56                                           categories_sorted_by_name)
57 from softwarecenter.db.utils import get_query_for_pkgnames
58 from softwarecenter.distro import get_distro
59 from softwarecenter.backend.scagent import SoftwareCenterAgent
60 from softwarecenter.backend.reviews import get_review_loader
61
62 LOG = logging.getLogger(__name__)
63
64
65 _asset_cache = {}
66
67
68 class CategoriesViewGtk(Viewport, CategoriesParser):
69
70     __gsignals__ = {
71         "category-selected": (GObject.SignalFlags.RUN_LAST,
72                               None,
73                               (GObject.TYPE_PYOBJECT, ),
74                              ),
75
76         "application-selected": (GObject.SignalFlags.RUN_LAST,
77                                  None,
78                                  (GObject.TYPE_PYOBJECT, ),
79                                 ),
80
81         "application-activated": (GObject.SignalFlags.RUN_LAST,
82                                   None,
83                                   (GObject.TYPE_PYOBJECT, ),
84                                  ),
85
86         "show-category-applist": (GObject.SignalFlags.RUN_LAST,
87                                   None,
88                                   (),)
89         }
90
91     SPACING = PADDING = 3
92
93     # art stuff
94     STIPPLE = os.path.join(softwarecenter.paths.datadir,
95                            "ui/gtk3/art/stipple.png")
96
97     def __init__(self,
98                  datadir,
99                  desktopdir,
100                  cache,
101                  db,
102                  icons,
103                  apps_filter=None,  # FIXME: kill this, its not needed anymore?
104                  apps_limit=0):
105
106         """ init the widget, takes
107
108         datadir - the base directory of the app-store data
109         desktopdir - the dir where the applications.menu file can be found
110         db - a Database object
111         icons - a Gtk.IconTheme
112         apps_filter - ?
113         apps_limit - the maximum amount of items to display to query for
114         """
115
116         self.cache = cache
117         self.db = db
118         self.icons = icons
119         self.properties_helper = AppPropertiesHelper(
120             self.db, self.cache, self.icons)
121         self.section = None
122
123         Viewport.__init__(self)
124         CategoriesParser.__init__(self, db)
125
126         self.set_name("category-view")
127
128         # setup base widgets
129         # we have our own viewport so we know when the viewport grows/shrinks
130         # setup widgets
131
132         self.vbox = Gtk.VBox()
133         self.add(self.vbox)
134
135         # atk stuff
136         atk_desc = self.get_accessible()
137         atk_desc.set_name(_("Departments"))
138
139         # appstore stuff
140         self.categories = []
141         self.header = ""
142         #~ self.apps_filter = apps_filter
143         self.apps_limit = apps_limit
144         # for comparing on refreshes
145         self._supported_only = False
146
147         # more stuff
148         self._poster_sigs = []
149         self._allocation = None
150
151         self._cache_art_assets()
152         #~ assets = self._cache_art_assets()
153         #~ self.vbox.connect("draw", self.on_draw, assets)
154         self._prev_alloc = None
155         self.connect("size-allocate", self.on_size_allocate)
156         return
157
158     def _add_tiles_to_flowgrid(self, docs, flowgrid, amount):
159         '''Adds application tiles to a FlowableGrid:
160            docs = xapian documents (apps)
161            flowgrid = the FlowableGrid to add tiles to
162            amount = number of tiles to add from start of doc range'''
163         amount = min(len(docs), amount)
164         for doc in docs[0:amount]:
165             tile = FeaturedTile(self.properties_helper, doc)
166             tile.connect('clicked', self.on_app_clicked,
167                          self.properties_helper.get_application(doc))
168             flowgrid.add_child(tile)
169         return
170
171     def on_size_allocate(self, widget, _):
172         a = widget.get_allocation()
173         prev = self._prev_alloc
174         if prev is None or a.width != prev.width or a.height != prev.height:
175             self._prev_alloc = a
176             self.queue_draw()
177         return
178
179     def _cache_art_assets(self):
180         global _asset_cache
181         if _asset_cache:
182             return _asset_cache
183         assets = _asset_cache
184         # cache the bg pattern
185         surf = cairo.ImageSurface.create_from_png(self.STIPPLE)
186         ptrn = cairo.SurfacePattern(surf)
187         ptrn.set_extend(cairo.EXTEND_REPEAT)
188         assets["stipple"] = ptrn
189         return assets
190
191     def on_app_clicked(self, btn, app):
192         """emit the category-selected signal when a category was clicked"""
193         def timeout_emit():
194             self.emit("application-selected", app)
195             self.emit("application-activated", app)
196             return False
197
198         GObject.timeout_add(50, timeout_emit)
199
200     def on_category_clicked(self, btn, cat):
201         """emit the category-selected signal when a category was clicked"""
202         def timeout_emit():
203             self.emit("category-selected", cat)
204             return False
205
206         GObject.timeout_add(50, timeout_emit)
207
208     def build(self, desktopdir):
209         pass
210
211     def do_draw(self, cr):
212         cr.set_source(_asset_cache["stipple"])
213         cr.paint_with_alpha(0.5)
214         for child in self:
215             self.propagate_draw(child, cr)
216
217     def set_section(self, section):
218         self.section = section
219
220     def refresh_apps(self):
221         raise NotImplementedError
222
223
224 class LobbyViewGtk(CategoriesViewGtk):
225
226     def __init__(self, datadir, desktopdir, cache, db, icons,
227                  apps_filter, apps_limit=0):
228         CategoriesViewGtk.__init__(self, datadir, desktopdir, cache, db, icons,
229                                    apps_filter, apps_limit=0)
230         self.top_rated = None
231         self.exhibit_banner = None
232
233         # sections
234         self.departments = None
235         self.appcount = None
236
237         self.build(desktopdir)
238
239         # ensure that on db-reopen we refresh the whats-new titles
240         self.db.connect("reopen", self._on_db_reopen)
241
242         # ensure that updates to the stats are reflected in the UI
243         self.reviews_loader = get_review_loader(self.cache)
244         self.reviews_loader.connect(
245             "refresh-review-stats-finished", self._on_refresh_review_stats)
246
247     def _on_db_reopen(self, db):
248         self._update_whats_new_content()
249
250     def _on_refresh_review_stats(self, reviews_loader, review_stats):
251         self._update_top_rated_content()
252
253     def _build_homepage_view(self):
254         # these methods add sections to the page
255         # changing order of methods changes order that they appear in the page
256         self._append_banner_ads()
257
258         self.top_hbox = Gtk.HBox(spacing=StockEms.SMALL)
259         top_hbox_alignment = Gtk.Alignment()
260         top_hbox_alignment.set_padding(0, 0, StockEms.MEDIUM - 2,
261             StockEms.MEDIUM - 2)
262         top_hbox_alignment.add(self.top_hbox)
263         self.vbox.pack_start(top_hbox_alignment, False, False, 0)
264
265         self._append_departments()
266
267         self.right_column = Gtk.Box.new(Gtk.Orientation.VERTICAL, self.SPACING)
268         self.top_hbox.pack_start(self.right_column, True, True, 0)
269
270         self._append_whats_new()
271         self._append_top_rated()
272         self._append_recommended_for_you()
273         self._append_appcount()
274
275         #self._append_video_clips()
276         #self._append_top_of_the_pops
277
278     #~ def _append_top_of_the_pops(self):
279         #~ self.totp_hbox = Gtk.HBox(spacing=self.SPACING)
280 #~
281         #~ alignment = Gtk.Alignment()
282         #~ alignment.set_padding(0, 0, self.PADDING, self.PADDING)
283         #~ alignment.add(self.totp_hbox)
284 #~
285         #~ frame = FramedHeaderBox()
286         #~ frame.header_implements_more_button()
287         #~ frame.set_header_label(_("Most Popular"))
288 #~
289         #~ label = Gtk.Label.new("Soda pop!!!")
290         #~ label.set_name("placeholder")
291         #~ label.set_size_request(-1, 200)
292 #~
293         #~ frame.add(label)
294         #~ self.totp_hbox.add(frame)
295 #~
296         #~ frame = FramedHeaderBox()
297         #~ frame.header_implements_more_button()
298         #~ frame.set_header_label(_("Top Rated"))
299 #~
300         #~ label = Gtk.Label.new("Demos ftw(?)")
301         #~ label.set_name("placeholder")
302         #~ label.set_size_request(-1, 200)
303 #~
304         #~ frame.add(label)
305         #~ self.totp_hbox.add(frame)
306 #~
307         #~ self.vbox.pack_start(alignment, False, False, 0)
308         #~ return
309
310     #~ def _append_video_clips(self):
311         #~ frame = FramedHeaderBox()
312         #~ frame.set_header_expand(False)
313         #~ frame.set_header_position(HeaderPosition.LEFT)
314         #~ frame.set_header_label(_("Latest Demo Videos"))
315 #~
316         #~ label = Gtk.Label.new("Videos go here")
317         #~ label.set_name("placeholder")
318         #~ label.set_size_request(-1, 200)
319 #~
320         #~ frame.add(label)
321 #~
322         #~ alignment = Gtk.Alignment()
323         #~ alignment.set_padding(0, 0, self.PADDING, self.PADDING)
324         #~ alignment.add(frame)
325 #~
326         #~ self.vbox.pack_start(alignment, False, False, 0)
327         #~ return
328
329     def _on_show_exhibits(self, exhibit_banner, exhibit):
330         pkgs = exhibit.package_names.split(",")
331         if len(pkgs) == 1:
332             app = Application("", pkgs[0])
333             self.emit("application-activated", app)
334         else:
335             query = get_query_for_pkgnames(pkgs)
336             title = exhibit.title_translated
337             untranslated_name = exhibit.package_names
338             # create a temp query
339             cat = Category(untranslated_name, title, None, query,
340                            flags=['nonapps-visible'])
341             self.emit("category-selected", cat)
342
343     def _filter_and_set_exhibits(self, sca_client, exhibit_list):
344         result = []
345         # filter out those exhibits that are not available in this run
346         for exhibit in exhibit_list:
347             available = all(self.db.is_pkgname_known(p) for p in
348                             exhibit.package_names.split(','))
349             if available:
350                 result.append(exhibit)
351
352         # its ok if result is empty, since set_exhibits() will ignore
353         # empty lists
354         self.exhibit_banner.set_exhibits(result)
355
356     def _append_banner_ads(self):
357         self.exhibit_banner = ExhibitBanner()
358         self.exhibit_banner.set_exhibits([FeaturedExhibit()])
359         self.exhibit_banner.connect(
360             "show-exhibits-clicked", self._on_show_exhibits)
361
362         # query using the agent
363         scagent = SoftwareCenterAgent()
364         scagent.connect("exhibits", self._filter_and_set_exhibits)
365         scagent.query_exhibits()
366
367         a = Gtk.Alignment()
368         a.set_padding(0, StockEms.SMALL, 0, 0)
369         a.add(self.exhibit_banner)
370         self.vbox.pack_start(a, False, False, 0)
371
372     def _append_departments(self):
373         # set the departments section to use the label markup we have just
374         # defined
375         cat_vbox = FramedBox(Gtk.Orientation.VERTICAL)
376         self.top_hbox.pack_start(cat_vbox, False, False, 0)
377
378         # sort Category.name's alphabetically
379         sorted_cats = categories_sorted_by_name(self.categories)
380
381         mrkup = "<small>%s</small>"
382         for cat in sorted_cats:
383             if 'carousel-only' in cat.flags:
384                 continue
385             category_name = mrkup % GObject.markup_escape_text(cat.name)
386             label = LabelTile(category_name, None)
387             label.label.set_margin_left(StockEms.SMALL)
388             label.label.set_margin_right(StockEms.SMALL)
389             label.label.set_alignment(0.0, 0.5)
390             label.label.set_use_markup(True)
391             label.connect('clicked', self.on_category_clicked, cat)
392             cat_vbox.pack_start(label, False, False, 0)
393         return
394
395     # FIXME: _update_{top_rated,whats_new,recommended_for_you}_content()
396     #        duplicates a lot of code
397     def _update_top_rated_content(self):
398         # remove any existing children from the grid widget
399         self.top_rated.remove_all()
400         # get top_rated category and docs
401         top_rated_cat = get_category_by_name(
402             self.categories, u"Top Rated")  # untranslated name
403         if top_rated_cat:
404             docs = top_rated_cat.get_documents(self.db)
405             self._add_tiles_to_flowgrid(docs, self.top_rated,
406                                         TOP_RATED_CAROUSEL_LIMIT)
407             self.top_rated.show_all()
408         return top_rated_cat
409
410     def _append_top_rated(self):
411         self.top_rated = FlowableGrid()
412         #~ self.top_rated.row_spacing = StockEms.SMALL
413         self.top_rated_frame = FramedHeaderBox()
414         self.top_rated_frame.set_header_label(_("Top Rated"))
415         self.top_rated_frame.add(self.top_rated)
416         self.right_column.pack_start(self.top_rated_frame, True, True, 0)
417         top_rated_cat = self._update_top_rated_content()
418         # only display the 'More' LinkButton if we have top_rated content
419         if top_rated_cat is not None:
420             self.top_rated_frame.header_implements_more_button()
421             self.top_rated_frame.more.connect('clicked',
422                                self.on_category_clicked, top_rated_cat)
423         return
424
425     def _update_whats_new_content(self):
426         # remove any existing children from the grid widget
427         self.whats_new.remove_all()
428         # get top_rated category and docs
429         whats_new_cat = get_category_by_name(
430             self.categories, u"What\u2019s New")  # untranslated name
431         if whats_new_cat:
432             docs = whats_new_cat.get_documents(self.db)
433             self._add_tiles_to_flowgrid(docs, self.whats_new, 8)
434             self.whats_new.show_all()
435         return whats_new_cat
436
437     def _append_whats_new(self):
438         self.whats_new = FlowableGrid()
439         self.whats_new_frame = FramedHeaderBox()
440         self.whats_new_frame.set_header_label(_(u"What\u2019s New"))
441         self.whats_new_frame.add(self.whats_new)
442
443         whats_new_cat = self._update_whats_new_content()
444         if whats_new_cat is not None:
445             # only add to the visible right_frame if we actually have it
446             self.right_column.pack_start(self.whats_new_frame, True, True, 0)
447             self.whats_new_frame.header_implements_more_button()
448             self.whats_new_frame.more.connect(
449                 'clicked', self.on_category_clicked, whats_new_cat)
450
451     def _update_recommended_for_you_content(self):
452         if (self.recommended_for_you_panel and
453             self.recommended_for_you_panel.get_parent()):
454             self.bottom_hbox.remove(self.recommended_for_you_panel)
455         self.recommended_for_you_panel = RecommendationsPanelLobby(self)
456         self.bottom_hbox.pack_start(self.recommended_for_you_panel,
457                                     True, True, 0)
458
459     def _append_recommended_for_you(self):
460         # TODO: This space will initially contain an opt-in screen, and this
461         #       will update to the tile view of recommended apps when ready
462         #       see https://wiki.ubuntu.com/SoftwareCenter#Home_screen
463         self.bottom_hbox = Gtk.HBox(spacing=StockEms.SMALL)
464         bottom_hbox_alignment = Gtk.Alignment()
465         bottom_hbox_alignment.set_padding(0, 0, StockEms.MEDIUM - 2,
466             StockEms.MEDIUM - 2)
467         bottom_hbox_alignment.add(self.bottom_hbox)
468         self.vbox.pack_start(bottom_hbox_alignment, False, False, 0)
469
470         # TODO: During development, place the "Recommended For You" panel
471         #       at the bottom, but swap this with the Top Rated panel once
472         #       the recommended for you pieces are done and deployed
473         #       see https://wiki.ubuntu.com/SoftwareCenter#Home_screen
474         self.recommended_for_you_panel = RecommendationsPanelLobby(self)
475         self.bottom_hbox.pack_start(self.recommended_for_you_panel,
476                                     True, True, 0)
477
478     def _update_appcount(self):
479         enq = AppEnquire(self.cache, self.db)
480
481         distro = get_distro()
482         if get_global_filter().supported_only:
483             query = distro.get_supported_query()
484         else:
485             query = xapian.Query('')
486
487         length = enq.get_estimated_matches_count(query)
488         text = gettext.ngettext("%(amount)s item", "%(amount)s items", length
489                                 ) % {'amount': length}
490         self.appcount.set_text(text)
491
492     def _append_appcount(self):
493         self.appcount = Gtk.Label()
494         self.appcount.set_alignment(0.5, 0.5)
495         self.appcount.set_margin_top(1)
496         self.appcount.set_margin_bottom(4)
497         self.vbox.pack_start(self.appcount, False, True, 0)
498         self._update_appcount()
499         return
500
501     def build(self, desktopdir):
502         self.categories = self.parse_applications_menu(desktopdir)
503         self.header = _('Departments')
504         self._build_homepage_view()
505         self.show_all()
506         return
507
508     def refresh_apps(self):
509         supported_only = get_global_filter().supported_only
510         if (self._supported_only == supported_only):
511             return
512         self._supported_only = supported_only
513
514         self._update_top_rated_content()
515         self._update_whats_new_content()
516         self._update_recommended_for_you_content()
517         self._update_appcount()
518         return
519
520     # stubs for the time being, we may reuse them if we get dynamic content
521     # again
522     def stop_carousels(self):
523         pass
524
525     def start_carousels(self):
526         pass
527
528
529 class SubCategoryViewGtk(CategoriesViewGtk):
530
531     def __init__(self, datadir, desktopdir, cache, db, icons,
532                  apps_filter, apps_limit=0, root_category=None):
533         CategoriesViewGtk.__init__(self, datadir, desktopdir, cache, db, icons,
534                                    apps_filter, apps_limit)
535         # state
536         self._built = False
537         # data
538         self.root_category = root_category
539         self.enquire = AppEnquire(self.cache, self.db)
540         self.properties_helper = AppPropertiesHelper(
541             self.db, self.cache, self.icons)
542
543         # sections
544         self.current_category = None
545         self.departments = None
546         self.top_rated = None
547         self.recommended_for_you_in_cat = None
548         self.appcount = None
549
550         # widgetry
551         self.vbox.set_margin_left(StockEms.MEDIUM - 2)
552         self.vbox.set_margin_right(StockEms.MEDIUM - 2)
553         self.vbox.set_margin_top(StockEms.MEDIUM)
554         return
555
556     def _get_sub_top_rated_content(self, category):
557         app_filter = AppFilter(self.db, self.cache)
558         self.enquire.set_query(category.query,
559                                limit=TOP_RATED_CAROUSEL_LIMIT,
560                                sortmode=SortMethods.BY_TOP_RATED,
561                                filter=app_filter,
562                                nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE,
563                                nonblocking_load=False)
564         return self.enquire.get_documents()
565
566     @wait_for_apt_cache_ready  # be consistent with new apps
567     def _update_sub_top_rated_content(self, category):
568         self.top_rated.remove_all()
569         # FIXME: should this be m = "%s %s" % (_(gettext text), header text) ??
570         # TRANSLATORS: %s is a category name, like Internet or Development
571         # Tools
572         m = _('Top Rated %(category)s') % {
573             'category': GObject.markup_escape_text(self.header)}
574         self.top_rated_frame.set_header_label(m)
575         docs = self._get_sub_top_rated_content(category)
576         self._add_tiles_to_flowgrid(docs, self.top_rated,
577                                     TOP_RATED_CAROUSEL_LIMIT)
578         return
579
580     def _append_sub_top_rated(self):
581         self.top_rated = FlowableGrid()
582         self.top_rated.set_row_spacing(6)
583         self.top_rated.set_column_spacing(6)
584         self.top_rated_frame = FramedHeaderBox()
585         self.top_rated_frame.pack_start(self.top_rated, True, True, 0)
586         self.vbox.pack_start(self.top_rated_frame, False, True, 0)
587         return
588
589     def _update_recommended_for_you_in_cat_content(self, category):
590         if (self.recommended_for_you_in_cat and
591             self.recommended_for_you_in_cat.get_parent()):
592             self.vbox.remove(self.recommended_for_you_in_cat)
593         self.recommended_for_you_in_cat = RecommendationsPanelCategory(
594                                                                     self,
595                                                                     category)
596         # only show the panel in the categories view when the user
597         # is opted in to the recommender service
598         # FIXME: this is needed vs. a simple hide() on the widget because
599         #        we do a show_all on the view
600         if self.recommended_for_you_in_cat.recommender_agent.is_opted_in():
601             self.vbox.pack_start(self.recommended_for_you_in_cat,
602                                         False, False, 0)
603
604     def _update_subcat_departments(self, category, num_items):
605         self.departments.remove_all()
606
607         # set the subcat header
608         m = "<b><big>%s</big></b>"
609         self.subcat_label.set_markup(m % GObject.markup_escape_text(
610             self.header))
611
612         # sort Category.name's alphabetically
613         sorted_cats = categories_sorted_by_name(self.categories)
614         enquire = xapian.Enquire(self.db.xapiandb)
615         app_filter = AppFilter(self.db, self.cache)
616         for cat in sorted_cats:
617             # add the subcategory if and only if it is non-empty
618             enquire.set_query(cat.query)
619
620             if len(enquire.get_mset(0, 1)):
621                 tile = CategoryTile(cat.name, cat.iconname)
622                 tile.connect('clicked', self.on_category_clicked, cat)
623                 self.departments.add_child(tile)
624
625         # partialy work around a (quite rare) corner case
626         if num_items == 0:
627             enquire.set_query(xapian.Query(xapian.Query.OP_AND,
628                                 category.query,
629                                 xapian.Query("ATapplication")))
630             # assuming that we only want apps is not always correct ^^^
631             tmp_matches = enquire.get_mset(0, len(self.db), None, app_filter)
632             num_items = tmp_matches.get_matches_estimated()
633
634         # append an additional button to show all of the items in the category
635         all_cat = Category("All", _("All"), "category-show-all",
636             category.query)
637         name = GObject.markup_escape_text('%s %s' % (_("All"), num_items))
638         tile = CategoryTile(name, "category-show-all")
639         tile.connect('clicked', self.on_category_clicked, all_cat)
640         self.departments.add_child(tile)
641         self.departments.queue_draw()
642         return num_items
643
644     def _append_subcat_departments(self):
645         self.subcat_label = Gtk.Label()
646         self.subcat_label.set_alignment(0, 0.5)
647         self.departments = FlowableGrid(paint_grid_pattern=False)
648         self.departments.set_row_spacing(StockEms.SMALL)
649         self.departments.set_column_spacing(StockEms.SMALL)
650         self.departments_frame = FramedBox(spacing=StockEms.MEDIUM,
651                                            padding=StockEms.MEDIUM)
652         # set x/y-alignment and x/y-expand
653         self.departments_frame.set(0.5, 0.0, 1.0, 1.0)
654         self.departments_frame.pack_start(self.subcat_label, False, False, 0)
655         self.departments_frame.pack_start(self.departments, True, True, 0)
656         # append the departments section to the page
657         self.vbox.pack_start(self.departments_frame, False, True, 0)
658         return
659
660     def _update_appcount(self, appcount):
661         text = gettext.ngettext("%(amount)s item available",
662                                 "%(amount)s items available",
663                                 appcount) % {'amount': appcount}
664         self.appcount.set_text(text)
665         return
666
667     def _append_appcount(self):
668         self.appcount = Gtk.Label()
669         self.appcount.set_alignment(0.5, 0.5)
670         self.appcount.set_margin_top(1)
671         self.appcount.set_margin_bottom(4)
672         self.vbox.pack_end(self.appcount, False, False, 0)
673         return
674
675     def _build_subcat_view(self):
676         # these methods add sections to the page
677         # changing order of methods changes order that they appear in the page
678         self._append_subcat_departments()
679         self._append_sub_top_rated()
680         # NOTE that the recommended for you in category view is built and added
681         # in the _update_recommended_for_you_in_cat method (and so is not
682         # needed here)
683         self._append_appcount()
684         self._built = True
685         return
686
687     def _update_subcat_view(self, category, num_items=0):
688         num_items = self._update_subcat_departments(category, num_items)
689         self._update_sub_top_rated_content(category)
690         self._update_recommended_for_you_in_cat_content(category)
691         self._update_appcount(num_items)
692         self.show_all()
693         return
694
695     def set_subcategory(self, root_category, num_items=0, block=False):
696         # nothing to do
697         if (root_category is None or
698             self.categories == root_category.subcategories):
699             return
700
701         self.current_category = root_category
702         self.header = root_category.name
703         self.categories = root_category.subcategories
704
705         if not self._built:
706             self._build_subcat_view()
707         self._update_subcat_view(root_category, num_items)
708
709         GObject.idle_add(self.queue_draw)
710         return
711
712     def refresh_apps(self):
713         supported_only = get_global_filter().supported_only
714         if (self.current_category is None or
715             self._supported_only == supported_only):
716             return
717         self._supported_only = supported_only
718
719         if not self._built:
720             self._build_subcat_view()
721         self._update_subcat_view(self.current_category)
722         GObject.idle_add(self.queue_draw)
723         return
724
725     #def build(self, desktopdir):
726         #self.in_subsection = True
727         #self.set_subcategory(self.root_category)
728         #return
729
730
731 def get_test_window_catview():
732
733     def on_category_selected(view, cat):
734         print "on_category_selected view: ", view
735         print "on_category_selected cat: ", cat
736
737     from softwarecenter.db.pkginfo import get_pkg_info
738     cache = get_pkg_info()
739     cache.open()
740
741     from softwarecenter.db.database import StoreDatabase
742     xapian_base_path = "/var/cache/software-center"
743     pathname = os.path.join(xapian_base_path, "xapian")
744     db = StoreDatabase(pathname, cache)
745     db.open()
746
747     import softwarecenter.paths
748     datadir = softwarecenter.paths.datadir
749
750     from softwarecenter.ui.gtk3.utils import get_sc_icon_theme
751     icons = get_sc_icon_theme(datadir)
752
753     import softwarecenter.distro
754     distro = softwarecenter.distro.get_distro()
755
756     apps_filter = AppFilter(db, cache)
757
758     # gui
759     win = Gtk.Window()
760     notebook = Gtk.Notebook()
761
762     from softwarecenter.paths import APP_INSTALL_PATH
763     view = LobbyViewGtk(datadir, APP_INSTALL_PATH,
764                         cache, db, icons, distro, apps_filter)
765     win.set_data("lobby", view)
766
767     scroll = Gtk.ScrolledWindow()
768     scroll.add(view)
769     notebook.append_page(scroll, Gtk.Label(label="Lobby"))
770
771     # find a cat in the LobbyView that has subcategories
772     subcat_cat = None
773     for cat in reversed(view.categories):
774         if cat.subcategories:
775             subcat_cat = cat
776             break
777
778     view = SubCategoryViewGtk(datadir, APP_INSTALL_PATH, cache, db, icons,
779                               apps_filter)
780     view.connect("category-selected", on_category_selected)
781     view.set_subcategory(subcat_cat)
782     win.set_data("subcat", view)
783
784     scroll = Gtk.ScrolledWindow()
785     scroll.add(view)
786     notebook.append_page(scroll, Gtk.Label(label="Subcats"))
787
788     win.add(notebook)
789     win.set_size_request(800, 800)
790     win.show_all()
791     win.connect('destroy', Gtk.main_quit)
792     return win
793
794
795 def get_test_catview():
796
797     def on_category_selected(view, cat):
798         print("on_category_selected %s %s" % view, cat)
799
800     from softwarecenter.db.pkginfo import get_pkg_info
801     cache = get_pkg_info()
802     cache.open()
803
804     from softwarecenter.db.database import StoreDatabase
805     xapian_base_path = "/var/cache/software-center"
806     pathname = os.path.join(xapian_base_path, "xapian")
807     db = StoreDatabase(pathname, cache)
808     db.open()
809
810     import softwarecenter.paths
811     datadir = softwarecenter.paths.datadir
812
813     from softwarecenter.ui.gtk3.utils import get_sc_icon_theme
814     icons = get_sc_icon_theme(datadir)
815
816     import softwarecenter.distro
817     distro = softwarecenter.distro.get_distro()
818
819     apps_filter = AppFilter(db, cache)
820
821     from softwarecenter.paths import APP_INSTALL_PATH
822     cat_view = LobbyViewGtk(datadir, APP_INSTALL_PATH,
823                         cache, db, icons, distro, apps_filter)
824     return cat_view
825
826 if __name__ == "__main__":
827     import os
828     logging.basicConfig(level=logging.DEBUG)
829
830     win = get_test_window_catview()
831
832     # run it
833     Gtk.main()