1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2010 Canonical
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.
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
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/>.
33
from gettext import gettext as _
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
42
class HistoryPane(gtk.VBox, BasePane):
45
"app-list-changed" : (gobject.SIGNAL_RUN_LAST,
49
"history-pane-created" : (gobject.SIGNAL_RUN_FIRST,
54
(COL_WHEN, COL_ACTION, COL_PKG) = range(3)
55
COL_TYPES = (object, int, str)
57
(ALL, INSTALLED, REMOVED, UPGRADED) = range(4)
62
# pages for the spinner notebook
64
PAGE_SPINNER) = range(2)
66
def __init__(self, cache, db, distro, icons, datadir):
67
gtk.VBox.__init__(self)
72
self.datadir = datadir
74
self.apps_filter = None
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)
81
self.header = gtk.HBox()
83
self.pack_start(self.header, expand=False, padding=self.PADDING)
85
self.title = gtk.Label()
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)
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)
96
self.pack_start(gtk.HSeparator(), expand=False)
98
self.toolbar = gtk.Toolbar()
100
self.toolbar.set_style(gtk.TOOLBAR_TEXT)
101
self.pack_start(self.toolbar, expand=False)
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)
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)
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)
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)
125
self._actions_list = all_action.get_group()
126
self._set_actions_sensitive(False)
128
self.view = gtk.TreeView()
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)
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)
144
self.pack_start(self.spinner_notebook)
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")
155
# to save (a lot of) time at startup we load history later, only when
156
# it is selected to be viewed
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)
169
self.busy_cursor = gtk.gdk.Cursor(gtk.gdk.WATCH)
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")
185
def _set_actions_sensitive(self, sensitive):
186
for action in self._actions_list:
187
action.set_sensitive(sensitive)
189
def _reset_icon_cache(self, theme=None):
190
self._app_icon_cache.clear()
192
missing = self.icons.load_icon(MISSING_APP_ICON, self.ICON_SIZE, 0)
195
self._app_icon_cache[MISSING_APP_ICON] = missing
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():
205
self.history.set_on_update(self.parse_history)
207
def parse_history(self):
211
day = self.store.get_iter_first()
213
date = self.store.get_value(day, self.COL_WHEN)
214
if len(self.history.transactions) == 0:
215
logging.debug("AptHistory is currently empty")
217
new_last = self.history.transactions[0].start_date
218
for trans in self.history.transactions:
219
while gtk.events_pending():
221
when = trans.start_date
222
if self.last is not None and when <= self.last:
224
if when.date() != date:
226
day = self.store.append(None, (date, self.ALL, None))
228
actions = {self.INSTALLED: trans.install,
229
self.REMOVED: trans.remove,
230
self.UPGRADED: trans.upgrade,
232
for action, pkgs in actions.iteritems():
234
row = (when, action, pkgname)
235
last_row = self.store.insert_after(day, last_row, row)
239
def get_status_text(self):
240
return _('%d changes') % self.visible_changes
242
def on_search_terms_changed(self, entry, terms):
245
def change_filter(self, action, current):
246
self.filter = action.get_current_value()
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()
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)
263
# Expand the most recent day
264
day = self.store.get_iter_first()
266
path = self.store.get_path(day)
267
self.view.expand_row(path, False)
268
self.view.scroll_to_cell(path)
270
self.emit('app-list-changed', self.visible_changes)
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
280
def filter_row(self, store, iter):
281
pkg = store.get_value(iter, self.COL_PKG)
283
return self._row_matches(store, iter)
285
i = store.iter_children(iter)
287
if self._row_matches(store, i):
289
i = store.iter_next(i)
292
def render_cell_icon(self, column, cell, store, iter):
293
pkg = store.get_value(iter, self.COL_PKG)
295
cell.set_visible(False)
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)
303
icon_name = os.path.splitext(icon_value)[0]
305
if icon_name in self._app_icon_cache:
306
icon = self._app_icon_cache[icon_name]
309
icon = self.icons.load_icon(icon_name, self.ICON_SIZE, 0)
311
icon = self._app_icon_cache[MISSING_APP_ICON]
312
self._app_icon_cache[icon_name] = icon
313
cell.set_property('pixbuf', icon)
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())
332
# Current week, display the name of the day
333
text = when.strftime(_('%A'))
335
if when.year == today.year:
336
# Current year, display the day and month
337
text = when.strftime(_('%d %B'))
339
# Display the full date: day, month, year
340
text = when.strftime(_('%d %B %Y'))
341
cell.set_property('text', text)
344
if __name__ == '__main__':
347
db_path = os.path.join(XAPIAN_BASE_PATH, "xapian")
348
db = StoreDatabase(db_path, cache)
351
icons = gtk.icon_theme_get_default()
352
icons.append_search_path(ICON_PATH)
354
widget = HistoryPane(cache, db, None, icons, None)
357
window = gtk.Window()
359
window.set_size_request(600, 500)
360
window.set_position(gtk.WIN_POS_CENTER)
363
window.connect('destroy', gtk.main_quit)