~brian-murray/software-center/apport-hook

« back to all changes in this revision

Viewing changes to softwarecenter/view/historypane.py

  • Committer: Bazaar Package Importer
  • Author(s): Michael Vogt, Michael Vogt, Gary Lasker
  • Date: 2011-05-24 12:10:09 UTC
  • Revision ID: james.westby@ubuntu.com-20110524121009-70qj02d2iz4hzb2q
Tags: 4.1.3
[ Michael Vogt ]
* merged lp:~mvo/software-center/refactor, no user visible
  changes, only code cleanup
* merged lp:~mvo/software-center/pyflakes, no user visible
  changes, only code cleanup
* enforce pyflakes cleaness on bzr-buildpackage
* merged lp:~mvo/software-center/review-language-fallback
  to support fallback to other languages if there are no
  reviews in the native language
* merged lp:~evfool/software-center/fix506419, many thanks!
  (LP: #506419)

[ Gary Lasker ]
* softwarecenter/app.py:
  - expand the "Get Software" item in the viewswitcher by default
    so that its subitems are always visible and available
    (LP: #774590)
* merged lp:~aaronp/software-center/refactoring, many thanks
  to Aaron Peachey
* softwarecenter/app.py,
  softwarecenter/backend/restfulclient.py,
  softwarecenter/backend/rnrclient.py,
  softwarecenter/db/update.py:
  - fix some root logger warnings, other cleanup
* merged lp:~evfool/software-center/carouseltransition, very
  nice effect, many thanks Robert Roth (LP: #633193)
* softwarecenter/ui/gtk/availablepane.py,
  softwarecenter/ui/gtk/catview_gtk.py:
  - jumpstart Featured and What's New carousel transitions
    on launch (LP: #786403) 

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# Copyright (C) 2010 Canonical
4
 
#
5
 
# Authors:
6
 
#  Olivier Tilloy
7
 
#
8
 
# This program is free software; you can redistribute it and/or modify it under
9
 
# the terms of the GNU General Public License as published by the Free Software
10
 
# Foundation; version 3.
11
 
#
12
 
# This program is distributed in the hope that it will be useful, but WITHOUT
13
 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14
 
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
15
 
# details.
16
 
#
17
 
# You should have received a copy of the GNU General Public License along with
18
 
# this program.  If not, see <http://www.gnu.org/licenses/>.
19
 
 
20
 
 
21
 
import gobject
22
 
import gio
23
 
import glib
24
 
import gtk
25
 
import logging
26
 
 
27
 
import apt_pkg
28
 
apt_pkg.init_config()
29
 
 
30
 
import os.path
31
 
import datetime
32
 
 
33
 
from gettext import gettext as _
34
 
 
35
 
from softwarecenter.enums import *
36
 
from softwarecenter.view.widgets.searchentry import SearchEntry
37
 
from softwarecenter.view.widgets.spinner import SpinnerView
38
 
from softwarecenter.apt.aptcache import AptCache
39
 
from softwarecenter.db.database import StoreDatabase
40
 
from softwarecenter.view.basepane import BasePane
41
 
 
42
 
class HistoryPane(gtk.VBox, BasePane):
43
 
 
44
 
    __gsignals__ = {
45
 
        "app-list-changed" : (gobject.SIGNAL_RUN_LAST,
46
 
                              gobject.TYPE_NONE, 
47
 
                              (int, ),
48
 
                             ),
49
 
        "history-pane-created" : (gobject.SIGNAL_RUN_FIRST,
50
 
                                  gobject.TYPE_NONE,
51
 
                                  ()),
52
 
    }
53
 
 
54
 
    (COL_WHEN, COL_ACTION, COL_PKG) = range(3)
55
 
    COL_TYPES = (object, int, str)
56
 
 
57
 
    (ALL, INSTALLED, REMOVED, UPGRADED) = range(4)
58
 
 
59
 
    ICON_SIZE = 24
60
 
    PADDING = 6
61
 
    
62
 
    # pages for the spinner notebook
63
 
    (PAGE_HISTORY_VIEW,
64
 
     PAGE_SPINNER) = range(2)
65
 
 
66
 
    def __init__(self, cache, db, distro, icons, datadir):
67
 
        gtk.VBox.__init__(self)
68
 
        self.cache = cache
69
 
        self.db = db
70
 
        self.distro = distro
71
 
        self.icons = icons
72
 
        self.datadir = datadir
73
 
 
74
 
        self.apps_filter = None
75
 
 
76
 
        # Icon cache, invalidated upon icon theme changes
77
 
        self._app_icon_cache = {}
78
 
        self._reset_icon_cache()
79
 
        self.icons.connect('changed', self._reset_icon_cache)
80
 
 
81
 
        self.header = gtk.HBox()
82
 
        self.header.show()
83
 
        self.pack_start(self.header, expand=False, padding=self.PADDING)
84
 
 
85
 
        self.title = gtk.Label()
86
 
        self.title.show()
87
 
        self.title.set_alignment(0, 0)
88
 
        self.title.set_markup(_('<span size="x-large">History</span>'))
89
 
        self.header.pack_start(self.title, padding=self.PADDING)
90
 
 
91
 
        self.searchentry = SearchEntry()
92
 
        self.searchentry.connect('terms-changed', self.on_search_terms_changed)
93
 
        self.searchentry.show()
94
 
        self.header.pack_start(self.searchentry, expand=False, padding=self.PADDING)
95
 
 
96
 
        self.pack_start(gtk.HSeparator(), expand=False)
97
 
 
98
 
        self.toolbar = gtk.Toolbar()
99
 
        self.toolbar.show()
100
 
        self.toolbar.set_style(gtk.TOOLBAR_TEXT)
101
 
        self.pack_start(self.toolbar, expand=False)
102
 
 
103
 
        all_action = gtk.RadioAction('filter_all', _('All Changes'), None, None, self.ALL)
104
 
        all_action.connect('changed', self.change_filter)
105
 
        all_button = all_action.create_tool_item()
106
 
        self.toolbar.insert(all_button, 0)
107
 
 
108
 
        installs_action = gtk.RadioAction('filter_installs', _('Installations'), None, None, self.INSTALLED)
109
 
        installs_action.set_group(all_action)
110
 
        installs_button = installs_action.create_tool_item()
111
 
        self.toolbar.insert(installs_button, 1)
112
 
 
113
 
        upgrades_action = gtk.RadioAction(
114
 
            'filter_upgrads', _('Updates'), None, None, self.UPGRADED)
115
 
        upgrades_action.set_group(all_action)
116
 
        upgrades_button = upgrades_action.create_tool_item()
117
 
        self.toolbar.insert(upgrades_button, 2)
118
 
 
119
 
        removals_action = gtk.RadioAction(
120
 
            'filter_removals', _('Removals'), None, None, self.REMOVED)
121
 
        removals_action.set_group(all_action)
122
 
        removals_button = removals_action.create_tool_item()
123
 
        self.toolbar.insert(removals_button, 3)
124
 
        
125
 
        self._actions_list = all_action.get_group()
126
 
        self._set_actions_sensitive(False)
127
 
 
128
 
        self.view = gtk.TreeView()
129
 
        self.view.show()
130
 
        self.history_view = gtk.ScrolledWindow()
131
 
        self.history_view.set_policy(gtk.POLICY_AUTOMATIC,
132
 
                                      gtk.POLICY_AUTOMATIC)
133
 
        self.history_view.show()
134
 
        self.history_view.add(self.view)
135
 
        
136
 
        # make a spinner to display while history is loading
137
 
        self.spinner_view = SpinnerView(_('Loading history'))
138
 
        self.spinner_notebook = gtk.Notebook()
139
 
        self.spinner_notebook.set_show_tabs(False)
140
 
        self.spinner_notebook.set_show_border(False)
141
 
        self.spinner_notebook.append_page(self.history_view)
142
 
        self.spinner_notebook.append_page(self.spinner_view)
143
 
        
144
 
        self.pack_start(self.spinner_notebook)
145
 
 
146
 
        self.store = gtk.TreeStore(*self.COL_TYPES)
147
 
        self.visible_changes = 0
148
 
        self.store_filter = self.store.filter_new()
149
 
        self.store_filter.set_visible_func(self.filter_row)
150
 
        self.view.set_model(self.store_filter)
151
 
        all_action.set_active(True)
152
 
        self.filename = apt_pkg.config.find_file("Dir::Log::History")
153
 
        self.last = None
154
 
        
155
 
        # to save (a lot of) time at startup we load history later, only when
156
 
        # it is selected to be viewed
157
 
        self.history = None
158
 
 
159
 
        self.column = gtk.TreeViewColumn(_('Date'))
160
 
        self.view.append_column(self.column)
161
 
        self.cell_icon = gtk.CellRendererPixbuf()
162
 
        self.column.pack_start(self.cell_icon, False)
163
 
        self.column.set_cell_data_func(self.cell_icon, self.render_cell_icon)
164
 
        self.cell_text = gtk.CellRendererText()
165
 
        self.column.pack_start(self.cell_text)
166
 
        self.column.set_cell_data_func(self.cell_text, self.render_cell_text)
167
 
        
168
 
        # busy cursor
169
 
        self.busy_cursor = gtk.gdk.Cursor(gtk.gdk.WATCH)
170
 
        
171
 
    def init_view(self):
172
 
        if self.history == None:
173
 
            # if the history is not yet initialized we have to load and parse it
174
 
            # show a spinner while we do that
175
 
            self.window.set_cursor(self.busy_cursor)
176
 
            self.spinner_view.start()
177
 
            self.spinner_notebook.set_current_page(self.PAGE_SPINNER)
178
 
            self.load_and_parse_history()
179
 
            self.spinner_notebook.set_current_page(self.PAGE_HISTORY_VIEW)
180
 
            self.spinner_view.stop()
181
 
            self._set_actions_sensitive(True)
182
 
            self.window.set_cursor(None)
183
 
            self.emit("history-pane-created")
184
 
            
185
 
    def _set_actions_sensitive(self, sensitive):
186
 
        for action in self._actions_list:
187
 
            action.set_sensitive(sensitive)
188
 
 
189
 
    def _reset_icon_cache(self, theme=None):
190
 
        self._app_icon_cache.clear()
191
 
        try:
192
 
            missing = self.icons.load_icon(MISSING_APP_ICON, self.ICON_SIZE, 0)
193
 
        except glib.GError:
194
 
            missing = None
195
 
        self._app_icon_cache[MISSING_APP_ICON] = missing
196
 
        
197
 
    def load_and_parse_history(self):
198
 
        from softwarecenter.apt.apthistory import get_apt_history
199
 
        self.history = get_apt_history()
200
 
        # FIXME: a signal from AptHistory is nicer
201
 
        while not self.history.history_ready:
202
 
            while gtk.events_pending():
203
 
                gtk.main_iteration()
204
 
        self.parse_history()
205
 
        self.history.set_on_update(self.parse_history)
206
 
 
207
 
    def parse_history(self):
208
 
        date = None
209
 
        when = None
210
 
        last_row = None
211
 
        day = self.store.get_iter_first()
212
 
        if day is not None:
213
 
            date = self.store.get_value(day, self.COL_WHEN)
214
 
        if len(self.history.transactions) == 0:
215
 
            logging.debug("AptHistory is currently empty")
216
 
            return
217
 
        new_last = self.history.transactions[0].start_date
218
 
        for trans in self.history.transactions:
219
 
            while gtk.events_pending():
220
 
                gtk.main_iteration()
221
 
            when = trans.start_date
222
 
            if self.last is not None and when <= self.last:
223
 
                break
224
 
            if when.date() != date:
225
 
                date = when.date()
226
 
                day = self.store.append(None, (date, self.ALL, None))
227
 
                last_row = None
228
 
            actions = {self.INSTALLED: trans.install, 
229
 
                       self.REMOVED: trans.remove,
230
 
                       self.UPGRADED: trans.upgrade,
231
 
                      }
232
 
            for action, pkgs in actions.iteritems():
233
 
                for pkgname in pkgs:
234
 
                    row = (when, action, pkgname)
235
 
                    last_row = self.store.insert_after(day, last_row, row)
236
 
        self.last = new_last
237
 
        self.update_view()
238
 
 
239
 
    def get_status_text(self):
240
 
        return _('%d changes') % self.visible_changes
241
 
 
242
 
    def on_search_terms_changed(self, entry, terms):
243
 
        self.update_view()
244
 
 
245
 
    def change_filter(self, action, current):
246
 
        self.filter = action.get_current_value()
247
 
        self.update_view()
248
 
 
249
 
    def update_view(self):
250
 
        self.store_filter.refilter()
251
 
        self.view.collapse_all()
252
 
        # Expand all the matching rows
253
 
        if self.searchentry.get_text():
254
 
            self.view.expand_all()
255
 
 
256
 
        # Compute the number of visible changes
257
 
        self.visible_changes = 0
258
 
        day = self.store_filter.get_iter_first()
259
 
        while day is not None:
260
 
            self.visible_changes += self.store_filter.iter_n_children(day)
261
 
            day = self.store_filter.iter_next(day)
262
 
            
263
 
        # Expand the most recent day
264
 
        day = self.store.get_iter_first()
265
 
        if day is not None:
266
 
                path = self.store.get_path(day)
267
 
                self.view.expand_row(path, False)
268
 
                self.view.scroll_to_cell(path)
269
 
 
270
 
        self.emit('app-list-changed', self.visible_changes)
271
 
 
272
 
    def _row_matches(self, store, iter):
273
 
        # Whether a child row matches the current filter and the search entry
274
 
        pkg = store.get_value(iter, self.COL_PKG) or ''
275
 
        filter_values = (self.ALL, store.get_value(iter, self.COL_ACTION))
276
 
        filter_matches = self.filter in filter_values
277
 
        search_matches = self.searchentry.get_text().lower() in pkg.lower()
278
 
        return filter_matches and search_matches
279
 
 
280
 
    def filter_row(self, store, iter):
281
 
        pkg = store.get_value(iter, self.COL_PKG)
282
 
        if pkg is not None:
283
 
            return self._row_matches(store, iter)
284
 
        else:
285
 
            i = store.iter_children(iter)
286
 
            while i is not None:
287
 
                if self._row_matches(store, i):
288
 
                    return True
289
 
                i = store.iter_next(i)
290
 
            return False
291
 
 
292
 
    def render_cell_icon(self, column, cell, store, iter):
293
 
        pkg = store.get_value(iter, self.COL_PKG)
294
 
        if pkg is None:
295
 
            cell.set_visible(False)
296
 
        else:
297
 
            cell.set_visible(True)
298
 
            icon_name = MISSING_APP_ICON
299
 
            for m in self.db.xapiandb.postlist("AP" + pkg):
300
 
                doc = self.db.xapiandb.get_document(m.docid)
301
 
                icon_value = doc.get_value(XAPIAN_VALUE_ICON)
302
 
                if icon_value:
303
 
                    icon_name = os.path.splitext(icon_value)[0]
304
 
                break
305
 
            if icon_name in self._app_icon_cache:
306
 
                icon = self._app_icon_cache[icon_name]
307
 
            else:
308
 
                try:
309
 
                    icon = self.icons.load_icon(icon_name, self.ICON_SIZE, 0)
310
 
                except glib.GError:
311
 
                    icon = self._app_icon_cache[MISSING_APP_ICON]
312
 
                self._app_icon_cache[icon_name] = icon
313
 
            cell.set_property('pixbuf', icon)
314
 
 
315
 
    def render_cell_text(self, column, cell, store, iter):
316
 
        when = store.get_value(iter, self.COL_WHEN)
317
 
        if isinstance(when, datetime.datetime):
318
 
            action = store.get_value(iter, self.COL_ACTION)
319
 
            pkg = store.get_value(iter, self.COL_PKG)
320
 
            # Translators : time displayed in history, display hours (0-12), minutes and AM/PM. %H should be used instead of %I to display hours 0-24
321
 
            subs = {'pkgname': pkg, 'time': when.time().strftime(_('%I:%M %p'))}
322
 
            if action == self.INSTALLED:
323
 
                text = _('%(pkgname)s installed %(time)s') % subs
324
 
            elif action == self.REMOVED:
325
 
                text = _('%(pkgname)s removed %(time)s') % subs
326
 
            elif action == self.UPGRADED:
327
 
                text = _('%(pkgname)s updated %(time)s') % subs
328
 
        elif isinstance(when, datetime.date):
329
 
            today = datetime.date.today()
330
 
            monday = today - datetime.timedelta(days=today.weekday())
331
 
            if when >= monday:
332
 
                # Current week, display the name of the day
333
 
                text = when.strftime(_('%A'))
334
 
            else:
335
 
                if when.year == today.year:
336
 
                    # Current year, display the day and month
337
 
                    text = when.strftime(_('%d %B'))
338
 
                else:
339
 
                    # Display the full date: day, month, year
340
 
                    text = when.strftime(_('%d %B %Y'))
341
 
        cell.set_property('text', text)
342
 
 
343
 
 
344
 
if __name__ == '__main__':
345
 
    cache = AptCache()
346
 
 
347
 
    db_path = os.path.join(XAPIAN_BASE_PATH, "xapian")
348
 
    db = StoreDatabase(db_path, cache)
349
 
    db.open()
350
 
 
351
 
    icons = gtk.icon_theme_get_default()
352
 
    icons.append_search_path(ICON_PATH)
353
 
 
354
 
    widget = HistoryPane(cache, db, None, icons, None)
355
 
    widget.show()
356
 
 
357
 
    window = gtk.Window()
358
 
    window.add(widget)
359
 
    window.set_size_request(600, 500)
360
 
    window.set_position(gtk.WIN_POS_CENTER)
361
 
    window.show_all()
362
 
    widget.init_view()
363
 
    window.connect('destroy', gtk.main_quit)
364
 
 
365
 
    gtk.main()
366