Merge lp:~kiwinote/software-center/less-is-more2 into lp:software-center
- less-is-more2
- Merge into trunk
Proposed by
Kiwinote
Status: | Merged |
---|---|
Merged at revision: | 2896 |
Proposed branch: | lp:~kiwinote/software-center/less-is-more2 |
Merge into: | lp:software-center |
Diff against target: |
2286 lines (+53/-2058) 13 files modified
debian/control (+0/-1) po/POTFILES.in (+0/-2) softwarecenter/backend/installbackend_impl/packagekit_enums.py (+0/-28) softwarecenter/backend/installbackend_impl/packagekitd.py (+2/-3) softwarecenter/backend/zeitgeist_simple.py (+0/-139) softwarecenter/paths.py (+5/-14) softwarecenter/toolkit.py (+0/-18) softwarecenter/ui/gtk3/panes/unused__channelpane.py (+0/-316) softwarecenter/ui/gtk3/shapes.py (+0/-257) softwarecenter/ui/gtk3/views/appdetailsview.py (+0/-40) softwarecenter/ui/gtk3/widgets/exhibits.py (+14/-4) softwarecenter/ui/gtk3/widgets/stars.py (+32/-1) softwarecenter/ui/gtk3/widgets/unused__pathbar.py (+0/-1235) |
To merge this branch: | bzr merge lp:~kiwinote/software-center/less-is-more2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Vogt | Approve | ||
Review via email: mp+98297@code.launchpad.net |
Commit message
Description of the change
this is the second part of less-is-more which is an initial run through the softwarecenter/ folder
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 'debian/control' |
2 | --- debian/control 2012-02-07 15:19:12 +0000 |
3 | +++ debian/control 2012-03-19 22:53:18 +0000 |
4 | @@ -52,7 +52,6 @@ |
5 | update-notifier, |
6 | software-properties-gtk, |
7 | sessioninstaller, |
8 | - zeitgeist-core, |
9 | lzma |
10 | Conflicts: gnome-app-install (<< 1), oneconf (<< 0.2.6.1) |
11 | Replaces: gnome-app-install |
12 | |
13 | === modified file 'po/POTFILES.in' |
14 | --- po/POTFILES.in 2012-03-19 21:01:41 +0000 |
15 | +++ po/POTFILES.in 2012-03-19 22:53:18 +0000 |
16 | @@ -48,7 +48,6 @@ |
17 | softwarecenter/ui/gtk3/panes/installedpane.py |
18 | softwarecenter/ui/gtk3/panes/pendingpane.py |
19 | softwarecenter/ui/gtk3/panes/softwarepane.py |
20 | -softwarecenter/ui/gtk3/panes/unused__channelpane.py |
21 | softwarecenter/ui/gtk3/panes/viewswitcher.py |
22 | softwarecenter/ui/gtk3/views/appdetailsview.py |
23 | softwarecenter/ui/gtk3/views/appview.py |
24 | @@ -65,7 +64,6 @@ |
25 | softwarecenter/ui/gtk3/widgets/searchentry.py |
26 | softwarecenter/ui/gtk3/widgets/stars.py |
27 | softwarecenter/ui/gtk3/widgets/thumbnail.py |
28 | -softwarecenter/ui/gtk3/widgets/unused__pathbar.py |
29 | softwarecenter/ui/gtk3/widgets/videoplayer.py |
30 | softwarecenter/ui/gtk3/widgets/weblivedialog.py |
31 | softwarecenter/ui/gtk3/widgets/recommendations.py |
32 | |
33 | === removed file 'softwarecenter/backend/installbackend_impl/packagekit_enums.py' |
34 | --- softwarecenter/backend/installbackend_impl/packagekit_enums.py 2012-03-16 20:12:57 +0000 |
35 | +++ softwarecenter/backend/installbackend_impl/packagekit_enums.py 1970-01-01 00:00:00 +0000 |
36 | @@ -1,28 +0,0 @@ |
37 | -# Copyright (C) 2007-2008 Richard Hughes <richard@hughsie.com> |
38 | -# 2011 Giovanni Campagna <scampa.giovanni@gmail.com> |
39 | -# |
40 | -# This program is free software; you can redistribute it and/or modify it under |
41 | -# the terms of the GNU General Public License as published by the Free Software |
42 | -# Foundation; version 3. |
43 | -# |
44 | -# This program is distributed in the hope that it will be useful, but WITHOUT |
45 | -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
46 | -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
47 | -# details. |
48 | -# |
49 | -# You should have received a copy of the GNU General Public License along with |
50 | -# this program; if not, write to the Free Software Foundation, Inc., |
51 | -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
52 | - |
53 | -# stolen from gnome-packagekit, which is GPL2+ |
54 | - |
55 | -from gi.repository import PackageKitGlib as packagekit |
56 | - |
57 | - |
58 | -# this requires packagekit 0.7.2 or better |
59 | -def status_enum_to_localised_text(status): |
60 | - return packagekit.info_enum_to_localised_present(status) |
61 | - |
62 | - |
63 | -def role_enum_to_localised_present(role): |
64 | - return packagekit.role_enum_to_localised_present(role) |
65 | |
66 | === modified file 'softwarecenter/backend/installbackend_impl/packagekitd.py' |
67 | --- softwarecenter/backend/installbackend_impl/packagekitd.py 2012-03-16 20:12:57 +0000 |
68 | +++ softwarecenter/backend/installbackend_impl/packagekitd.py 2012-03-19 22:53:18 +0000 |
69 | @@ -31,7 +31,6 @@ |
70 | TransactionProgress |
71 | ) |
72 | from softwarecenter.backend.installbackend import InstallBackend |
73 | -from softwarecenter.backend.installbackend_impl import packagekit_enums |
74 | |
75 | # temporary, must think of better solution |
76 | from softwarecenter.db.pkginfo import get_pkg_info |
77 | @@ -100,13 +99,13 @@ |
78 | def get_role_description(self, role=None): |
79 | role = role if role is not None else self._trans.get_property('role') |
80 | return self.meta_data.get('sc_appname', |
81 | - packagekit_enums.role_enum_to_localised_present(role)) |
82 | + packagekit.role_enum_to_localised_present(role)) |
83 | |
84 | def get_status_description(self, status=None): |
85 | if status is None: |
86 | status = self._trans.get_property('status') |
87 | |
88 | - return packagekit_enums.status_enum_to_localised_text(status) |
89 | + return packagekit.info_enum_to_localised_present(status) |
90 | |
91 | def is_waiting(self): |
92 | """ return true if a time consuming task is taking place """ |
93 | |
94 | === removed file 'softwarecenter/backend/zeitgeist_simple.py' |
95 | --- softwarecenter/backend/zeitgeist_simple.py 2012-03-19 13:35:47 +0000 |
96 | +++ softwarecenter/backend/zeitgeist_simple.py 1970-01-01 00:00:00 +0000 |
97 | @@ -1,139 +0,0 @@ |
98 | -# Copyright (C) 2009-2010 Canonical |
99 | -# |
100 | -# Authors: |
101 | -# Seif Lotfy |
102 | -# Michael Vogt |
103 | -# |
104 | -# This program is free software; you can redistribute it and/or modify it under |
105 | -# the terms of the GNU General Public License as published by the Free Software |
106 | -# Foundation; version 3. |
107 | -# |
108 | -# This program is distributed in the hope that it will be useful, but WITHOUT |
109 | -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
110 | -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
111 | -# details. |
112 | -# |
113 | -# You should have received a copy of the GNU General Public License along with |
114 | -# this program; if not, write to the Free Software Foundation, Inc., |
115 | -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
116 | - |
117 | -import logging |
118 | -import time |
119 | -LOG = logging.getLogger("sofwarecenter.zeitgeist") |
120 | - |
121 | -try: |
122 | - from zeitgeist.client import ZeitgeistClient |
123 | - from zeitgeist.datamodel import Event, Interpretation, ResultType |
124 | -except ImportError: |
125 | - LOG.exception("zeitgeist import failed") |
126 | - ZEITGEIST_AVAILABLE = False |
127 | -else: |
128 | - ZEITGEIST_AVAILABLE = True |
129 | - |
130 | - |
131 | -class SoftwareCenterZeitgeist(): |
132 | - """ simple wrapper around zeitgeist """ |
133 | - |
134 | - def __init__(self): |
135 | - try: |
136 | - self.zg_client = ZeitgeistClient() |
137 | - except Exception as e: |
138 | - logging.warn("can not get zeitgeist client: '%s'" % e) |
139 | - self.zg_client = None |
140 | - |
141 | - def get_usage_counter(self, application, callback, timerange=None): |
142 | - """Request the usage count as integer for the given application. |
143 | - When the request is there, "callback" is called. A optional |
144 | - timerange like [time.time(), time.time() - 30*24*60*60] can |
145 | - also be specified |
146 | - """ |
147 | - # helper |
148 | - def _callback(event_ids): |
149 | - callback(len(event_ids)) |
150 | - # no client or empty query -> empty result |
151 | - if not self.zg_client or not application: |
152 | - callback(0) |
153 | - return |
154 | - # the app we are looking for |
155 | - application = "application://" + application.split("/")[-1] |
156 | - # the event_templates |
157 | - e1 = Event.new_for_values( |
158 | - actor=application, interpretation=Interpretation.MODIFY_EVENT.uri) |
159 | - e2 = Event.new_for_values( |
160 | - actor=application, interpretation=Interpretation.CREATE_EVENT.uri) |
161 | - # run it |
162 | - self.zg_client.find_event_ids_for_templates( |
163 | - [e1, e2], _callback, timerange=timerange, num_events=0) |
164 | - |
165 | - def get_popular_mimetypes(self, callback, num=3): |
166 | - """ get the "num" (default to 3) most popular mimetypes based |
167 | - on the last 1000 events that zeitgeist recorded and |
168 | - call "callback" with [(count1, "mime1"), (count2, "mime2"), ...] |
169 | - as arguement |
170 | - """ |
171 | - def _callback(events): |
172 | - # gather |
173 | - mimetypes = {} |
174 | - for event in events: |
175 | - if event.subjects is None: |
176 | - continue |
177 | - mimetype = event.subjects[0].mimetype |
178 | - if not mimetype in mimetypes: |
179 | - mimetypes[mimetype] = 0 |
180 | - mimetypes[mimetype] += 1 |
181 | - # return early if empty |
182 | - results = [] |
183 | - if not mimetypes: |
184 | - callback([]) |
185 | - # convert to result and sort |
186 | - for k, v in mimetypes.items(): |
187 | - results.append([v, k]) |
188 | - results.sort(reverse=True) |
189 | - # tell the client about it |
190 | - callback(results[:num]) |
191 | - # no zeitgeist |
192 | - if not self.zg_client: |
193 | - return |
194 | - # trigger event (actual processing is done in _callback) |
195 | - # FIXME: investigate how result_type MostRecentEvents or |
196 | - # MostRecentSubjects would affect the results |
197 | - self.zg_client.find_events_for_templates( |
198 | - [], _callback, num_events=1000, |
199 | - result_type=ResultType.MostRecentEvents) |
200 | - |
201 | - |
202 | -class SoftwareCenterZeitgeistDummy(): |
203 | - def get_usage_counter(self, application, callback, timerange=None): |
204 | - callback(0) |
205 | - |
206 | - def get_popular_mimetypes(self, callback): |
207 | - callback([]) |
208 | - |
209 | -# singleton |
210 | -if ZEITGEIST_AVAILABLE: |
211 | - zeitgeist_singleton = SoftwareCenterZeitgeist() |
212 | -else: |
213 | - zeitgeist_singleton = SoftwareCenterZeitgeistDummy() |
214 | - |
215 | -if __name__ == "__main__": |
216 | - |
217 | - def _callback_counter(events): |
218 | - print("test _callback: %s" % events) |
219 | - # all time gedit |
220 | - zeitgeist_singleton.get_usage_counter("gedit.desktop", _callback_counter) |
221 | - |
222 | - # yesterday gedit |
223 | - end = time.time() |
224 | - start = end - 24 * 60 * 60 |
225 | - zeitgeist_singleton.get_usage_counter("gedit.desktop", _callback_counter, |
226 | - timerange=[start, end]) |
227 | - |
228 | - # most popular |
229 | - def _callback_popular(mimetypes): |
230 | - print("test _callback: ") |
231 | - for tuple in mimetypes: |
232 | - print(tuple) |
233 | - zeitgeist_singleton.get_popular_mimetypes(_callback_popular) |
234 | - |
235 | - from gi.repository import Gtk |
236 | - Gtk.main() |
237 | |
238 | === modified file 'softwarecenter/paths.py' |
239 | --- softwarecenter/paths.py 2012-03-15 22:36:31 +0000 |
240 | +++ softwarecenter/paths.py 2012-03-19 22:53:18 +0000 |
241 | @@ -34,8 +34,6 @@ |
242 | # xdg_cache_home=os.path.expanduser("~/.cache")) |
243 | from xdg import BaseDirectory as xdg |
244 | |
245 | -from softwarecenter.toolkit import CURRENT_TOOLKIT, UIToolkits |
246 | - |
247 | # global datadir, this maybe overriden at startup |
248 | datadir = "/usr/share/software-center/" |
249 | |
250 | @@ -70,18 +68,11 @@ |
251 | # ratings&review |
252 | # relative to datadir |
253 | class RNRApps: |
254 | - if CURRENT_TOOLKIT is UIToolkits.GTK2: |
255 | - SUBMIT_REVIEW = "submit_review.py" |
256 | - REPORT_REVIEW = "report_review.py" |
257 | - SUBMIT_USEFULNESS = "submit_usefulness.py" |
258 | - MODIFY_REVIEW = "modify_review.py" |
259 | - DELETE_REVIEW = "delete_review.py" |
260 | - elif CURRENT_TOOLKIT is UIToolkits.GTK3: |
261 | - SUBMIT_REVIEW = "submit_review_gtk3.py" |
262 | - REPORT_REVIEW = "report_review_gtk3.py" |
263 | - SUBMIT_USEFULNESS = "submit_usefulness_gtk3.py" |
264 | - MODIFY_REVIEW = "modify_review_gtk3.py" |
265 | - DELETE_REVIEW = "delete_review_gtk3.py" |
266 | + SUBMIT_REVIEW = "submit_review_gtk3.py" |
267 | + REPORT_REVIEW = "report_review_gtk3.py" |
268 | + SUBMIT_USEFULNESS = "submit_usefulness_gtk3.py" |
269 | + MODIFY_REVIEW = "modify_review_gtk3.py" |
270 | + DELETE_REVIEW = "delete_review_gtk3.py" |
271 | |
272 | |
273 | # piston helpers |
274 | |
275 | === removed file 'softwarecenter/toolkit.py' |
276 | --- softwarecenter/toolkit.py 2012-03-15 10:43:13 +0000 |
277 | +++ softwarecenter/toolkit.py 1970-01-01 00:00:00 +0000 |
278 | @@ -1,18 +0,0 @@ |
279 | -import sys |
280 | - |
281 | - |
282 | -class UIToolkits: |
283 | - GTK2 = 0 |
284 | - GTK3 = 1 |
285 | - QML = 2 |
286 | - FALLBACK = GTK3 |
287 | - |
288 | - |
289 | -if 'software-center' in sys.argv[0]: |
290 | - CURRENT_TOOLKIT = UIToolkits.GTK3 |
291 | -elif 'software-center-gtk2' in sys.argv[0]: |
292 | - CURRENT_TOOLKIT = UIToolkits.GTK2 |
293 | -elif 'software-center-qml' in sys.argv[0]: |
294 | - CURRENT_TOOLKIT = UIToolkits.QML |
295 | -else: |
296 | - CURRENT_TOOLKIT = UIToolkits.FALLBACK |
297 | |
298 | === removed file 'softwarecenter/ui/gtk3/models/navlogstore.py' |
299 | === removed file 'softwarecenter/ui/gtk3/panes/unused__channelpane.py' |
300 | --- softwarecenter/ui/gtk3/panes/unused__channelpane.py 2012-03-15 00:16:03 +0000 |
301 | +++ softwarecenter/ui/gtk3/panes/unused__channelpane.py 1970-01-01 00:00:00 +0000 |
302 | @@ -1,316 +0,0 @@ |
303 | -# Copyright (C) 2010 Canonical |
304 | -# |
305 | -# Authors: |
306 | -# Michael Vogt |
307 | -# Gary Lasker |
308 | -# |
309 | -# This program is free software; you can redistribute it and/or modify it under |
310 | -# the terms of the GNU General Public License as published by the Free Software |
311 | -# Foundation; version 3. |
312 | -# |
313 | -# This program is distributed in the hope that it will be useful, but WITHOUT |
314 | -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
315 | -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
316 | -# details. |
317 | -# |
318 | -# You should have received a copy of the GNU General Public License along with |
319 | -# this program; if not, write to the Free Software Foundation, Inc., |
320 | -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
321 | - |
322 | -import gettext |
323 | -from gi.repository import Gtk |
324 | -import logging |
325 | -import os |
326 | -import sys |
327 | -import xapian |
328 | -from gi.repository import GObject |
329 | - |
330 | -from gettext import gettext as _ |
331 | - |
332 | -from softwarecenter.backend import get_install_backend |
333 | -from softwarecenter.distro import get_distro |
334 | -from softwarecenter.enums import NavButtons, NonAppVisibility |
335 | -from softwarecenter.paths import XAPIAN_BASE_PATH |
336 | -from softwarepane import SoftwarePane |
337 | -from softwarecenter.ui.gtk3.views.appview import AppViewFilter |
338 | -import softwarecenter.ui.gtk3.dialogs as dialogs |
339 | - |
340 | -LOG = logging.getLogger(__name__) |
341 | - |
342 | - |
343 | -class ChannelPane(SoftwarePane): |
344 | - """Widget that represents the channel pane for display of |
345 | - individual channels (PPAs, partner repositories, etc.) |
346 | - in software-center. |
347 | - It contains a search entry and navigation buttons. |
348 | - """ |
349 | - |
350 | - (PAGE_APPLIST, |
351 | - PAGE_APP_DETAILS, |
352 | - PAGE_APP_PURCHASE) = range(3) |
353 | - |
354 | - __gsignals__ = {'channel-pane-created': (GObject.SignalFlags.RUN_FIRST, |
355 | - None, |
356 | - ())} |
357 | - |
358 | - def __init__(self, cache, db, distro, icons, datadir): |
359 | - # parent |
360 | - SoftwarePane.__init__(self, cache, db, distro, icons, datadir, |
361 | - show_ratings=False) |
362 | - self.channel = None |
363 | - self.apps_filter = None |
364 | - self.apps_search_term = "" |
365 | - self.current_appview_selection = None |
366 | - self.distro = get_distro() |
367 | - self.pane_name = _("Software Channels") |
368 | - |
369 | - def init_view(self): |
370 | - if not self.view_initialized: |
371 | - SoftwarePane.init_view(self) |
372 | - self.notebook.append_page(self.box_app_list, |
373 | - Gtk.Label(label="channel")) |
374 | - # details |
375 | - self.notebook.append_page(self.scroll_details, |
376 | - Gtk.Label(label="details")) |
377 | - # purchase view |
378 | - self.notebook.append_page(self.purchase_view, |
379 | - Gtk.Label(label="purchase")) |
380 | - # now we are initialized |
381 | - self.emit("channel-pane-created") |
382 | - self.show_all() |
383 | - self.view_initialized = True |
384 | - |
385 | - def _show_channel_overview(self): |
386 | - " helper that goes back to the overview page " |
387 | - self.navigation_bar.remove_ids(NavButtons.DETAILS) |
388 | - self.navigation_bar.remove_ids(NavButtons.PURCHASE) |
389 | - self.notebook.set_current_page(self.PAGE_APPLIST) |
390 | - self.searchentry.show() |
391 | - |
392 | - def _clear_search(self): |
393 | - # remove the details and clear the search |
394 | - self.searchentry.clear() |
395 | - self.navigation_bar.remove_ids(NavButtons.SEARCH) |
396 | - |
397 | - def set_channel(self, channel): |
398 | - """ |
399 | - set the current software channel object for display in the channel pane |
400 | - and set up the AppViewFilter if required |
401 | - """ |
402 | - self.channel = channel |
403 | - # check to see if there is any section info that needs to be applied |
404 | - # FIXME |
405 | - #~ if channel._channel_color: |
406 | - #~ self.section.set_color(channel._channel_color) |
407 | - #~ if channel._channel_view_id: |
408 | - #~ self.section.set_view_id(channel._channel_view_id) |
409 | - #~ self.section_sync() |
410 | - |
411 | - # check if the channel needs to added |
412 | - if channel.needs_adding and channel._source_entry: |
413 | - dialog = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL, |
414 | - type=Gtk.MessageType.QUESTION) |
415 | - dialog.set_title("") |
416 | - dialog.set_markup("<big><b>%s</b></big>" % _("Add channel")) |
417 | - dialog.format_secondary_text(_("The selected channel is not yet " |
418 | - "added. Do you want to add it now?")) |
419 | - dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, |
420 | - Gtk.STOCK_ADD, Gtk.ResponseType.YES) |
421 | - res = dialog.run() |
422 | - dialog.destroy() |
423 | - if res == Gtk.ResponseType.YES: |
424 | - channel.needs_adding = False |
425 | - backend = get_install_backend() |
426 | - backend.add_sources_list_entry(channel._source_entry) |
427 | - backend.emit("channels-changed", True) |
428 | - backend.reload() |
429 | - return |
430 | - # normal operation |
431 | - self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE |
432 | - self.apps_filter = None |
433 | - if self.channel.installed_only: |
434 | - if self.apps_filter is None: |
435 | - self.apps_filter = AppViewFilter(self.db, self.cache) |
436 | - self.apps_filter.set_installed_only(True) |
437 | - # switch to applist, this will clear searches too |
438 | - self.display_list() |
439 | - |
440 | - def on_search_terms_changed(self, searchentry, terms): |
441 | - """callback when the search entry widget changes""" |
442 | - LOG.debug("on_search_terms_changed: '%s'" % terms) |
443 | - self.apps_search_term = terms |
444 | - if not self.apps_search_term: |
445 | - self._clear_search() |
446 | - self.refresh_apps() |
447 | - self.notebook.set_current_page(self.PAGE_APPLIST) |
448 | - |
449 | - def on_db_reopen(self, db): |
450 | - LOG.debug("got db-reopen signal") |
451 | - self.refresh_apps() |
452 | - self.app_details_view.refresh_app() |
453 | - |
454 | - def on_navigation_search(self, button, part): |
455 | - """ callback when the navigation button with id 'search' is clicked""" |
456 | - self.display_search() |
457 | - |
458 | - def on_navigation_list(self, button, part): |
459 | - """callback when the navigation button with id 'list' is clicked""" |
460 | - if not button.get_active(): |
461 | - return |
462 | - self.display_list() |
463 | - |
464 | - def display_list(self): |
465 | - self._clear_search() |
466 | - self._show_channel_overview() |
467 | - # only emit something if the model is there |
468 | - model = self.app_view.get_model() |
469 | - if model: |
470 | - self.emit("app-list-changed", len(model)) |
471 | - |
472 | - def on_navigation_details(self, button, part): |
473 | - """callback when the navigation button with id 'details' is clicked""" |
474 | - if not button.get_active(): |
475 | - return |
476 | - self.display_details() |
477 | - |
478 | - def display_details(self): |
479 | - self.navigation_bar.remove_ids(NavButtons.PURCHASE) |
480 | - self.notebook.set_current_page(self.PAGE_APP_DETAILS) |
481 | - self.searchentry.hide() |
482 | - self.action_bar.clear() |
483 | - # we want to re-enable the buy button if this is an app for purchase |
484 | - # FIXME: hacky, find a better approach |
485 | - button = self.app_details_view.pkg_statusbar.button |
486 | - if button.get_label() == _(u'Buy\u2026'): |
487 | - button.set_sensitive(True) |
488 | - |
489 | - def on_navigation_purchase(self, button, part): |
490 | - """callback when the navigation button with id 'purchase' is clicked""" |
491 | - if not button.get_active(): |
492 | - return |
493 | - self.display_purchase() |
494 | - |
495 | - def display_purchase(self): |
496 | - self.notebook.set_current_page(self.PAGE_APP_PURCHASE) |
497 | - self.searchentry.hide() |
498 | - self.action_bar.clear() |
499 | - |
500 | - def on_application_selected(self, appview, app): |
501 | - """callback when an app is selected""" |
502 | - LOG.debug("on_application_selected: '%s'" % app) |
503 | - self.current_appview_selection = app |
504 | - |
505 | - def display_search(self): |
506 | - self.navigation_bar.remove_ids(NavButtons.DETAILS) |
507 | - self.navigation_bar.remove_ids(NavButtons.PURCHASE) |
508 | - self.notebook.set_current_page(self.PAGE_APPLIST) |
509 | - model = self.app_view.get_model() |
510 | - if model: |
511 | - length = len(self.app_view.get_model()) |
512 | - self.emit("app-list-changed", length) |
513 | - self.searchentry.show() |
514 | - |
515 | - def get_status_text(self): |
516 | - """return user readable status text suitable for a status bar""" |
517 | - # no status text in the details page |
518 | - if self.notebook.get_current_page() == self.PAGE_APP_DETAILS: |
519 | - return "" |
520 | - # otherwise, show status based on search or not |
521 | - model = self.app_view.get_model() |
522 | - if not model: |
523 | - return "" |
524 | - length = len(self.app_view.get_model()) |
525 | - if self.channel.installed_only: |
526 | - if len(self.searchentry.get_text()) > 0: |
527 | - return gettext.ngettext("%(amount)s matching item", |
528 | - "%(amount)s matching items", |
529 | - length) % {'amount': length} |
530 | - else: |
531 | - return gettext.ngettext("%(amount)s item installed", |
532 | - "%(amount)s items installed", |
533 | - length) % {'amount': length} |
534 | - else: |
535 | - if len(self.searchentry.get_text()) > 0: |
536 | - return gettext.ngettext("%(amount)s matching item", |
537 | - "%(amount)s matching items", |
538 | - length) % {'amount': length} |
539 | - else: |
540 | - return gettext.ngettext("%(amount)s item available", |
541 | - "%(amount)s items available", |
542 | - length) % {'amount': length} |
543 | - |
544 | - def get_current_app(self): |
545 | - """return the current active application object applicable |
546 | - to the context""" |
547 | - return self.current_appview_selection |
548 | - |
549 | - def is_category_view_showing(self): |
550 | - # there is no category view in the channel pane |
551 | - return False |
552 | - |
553 | - def is_applist_view_showing(self): |
554 | - """Return True if we are in the applist view """ |
555 | - return self.notebook.get_current_page() == self.PAGE_APPLIST |
556 | - |
557 | - def is_app_details_view_showing(self): |
558 | - """Return True if we are in the app_details view """ |
559 | - return self.notebook.get_current_page() == self.PAGE_APP_DETAILS |
560 | - |
561 | -if __name__ == "__main__": |
562 | - #logging.basicConfig(level=logging.DEBUG) |
563 | - |
564 | - if len(sys.argv) > 1: |
565 | - datadir = sys.argv[1] |
566 | - elif os.path.exists("./data"): |
567 | - datadir = "./data" |
568 | - else: |
569 | - datadir = "/usr/share/software-center" |
570 | - |
571 | - from softwarecenter.ui.gtk3.utils import get_sc_icon_theme |
572 | - icons = get_sc_icon_theme(datadir) |
573 | - |
574 | - from softwarecenter.db.database import StoreDatabase |
575 | - from softwarecenter.db.pkginfo import get_pkg_info |
576 | - cache = get_pkg_info() |
577 | - cache.open() |
578 | - |
579 | - # xapian |
580 | - xapian_base_path = XAPIAN_BASE_PATH |
581 | - pathname = os.path.join(xapian_base_path, "xapian") |
582 | - try: |
583 | - db = StoreDatabase(pathname, cache) |
584 | - db.open() |
585 | - except xapian.DatabaseOpeningError: |
586 | - # Couldn't use that folder as a database |
587 | - # This may be because we are in a bzr checkout and that |
588 | - # folder is empty. If the folder is empty, and we can find the |
589 | - # script that does population, populate a database in it. |
590 | - if os.path.isdir(pathname) and not os.listdir(pathname): |
591 | - from softwarecenter.db.update import rebuild_database |
592 | - logging.info("building local database") |
593 | - rebuild_database(pathname) |
594 | - db = StoreDatabase(pathname, cache) |
595 | - db.open() |
596 | - except xapian.DatabaseCorruptError as e: |
597 | - logging.exception("xapian open failed") |
598 | - dialogs.error(None, |
599 | - _("Sorry, can not open the software database"), |
600 | - _("Please re-install the 'software-center' " |
601 | - "package.")) |
602 | - # FIXME: force rebuild by providing a dbus service for this |
603 | - sys.exit(1) |
604 | - |
605 | - import softwarecenter.distro |
606 | - distro = softwarecenter.distro.get_distro() |
607 | - |
608 | - w = ChannelPane(cache, db, distro, icons, datadir) |
609 | - w.show() |
610 | - |
611 | - win = Gtk.Window() |
612 | - win.add(w) |
613 | - w.init_view() |
614 | - win.set_size_request(400, 600) |
615 | - win.show_all() |
616 | - win.connect("destroy", Gtk.main_quit) |
617 | - |
618 | - Gtk.main() |
619 | |
620 | === removed file 'softwarecenter/ui/gtk3/shapes.py' |
621 | --- softwarecenter/ui/gtk3/shapes.py 2012-03-15 09:32:18 +0000 |
622 | +++ softwarecenter/ui/gtk3/shapes.py 1970-01-01 00:00:00 +0000 |
623 | @@ -1,257 +0,0 @@ |
624 | -import gi |
625 | -gi.require_version("Gtk", "3.0") |
626 | -from gi.repository import Gtk |
627 | - |
628 | - |
629 | -from math import sin, cos |
630 | - |
631 | -# pi constants |
632 | -from math import pi as PI |
633 | -PI_OVER_180 = PI / 180 |
634 | - |
635 | - |
636 | -def radian(deg): |
637 | - return PI_OVER_180 * deg |
638 | - |
639 | -# directional shapes |
640 | - |
641 | - |
642 | -class Shape: |
643 | - |
644 | - """ Base class for a Shape implementation. |
645 | - |
646 | - Currently implements a single method <layout> which is called |
647 | - to layout the shape using cairo paths. It can also store the |
648 | - 'direction' of the shape which should be on of the Gtk.TEXT_DIR |
649 | - constants. Default 'direction' is Gtk.TextDirection.LTR. |
650 | - |
651 | - When implementing a Shape, there are two options available. |
652 | - |
653 | - If the Shape is direction dependent, the Shape MUST |
654 | - implement <_layout_ltr> and <_layout_rtl> methods. |
655 | - |
656 | - If the Shape is not direction dependent, then it simply can |
657 | - override the <layout> method. |
658 | - |
659 | - <layout> methods must take the following as arguments: |
660 | - |
661 | - cr : a CairoContext |
662 | - x : x coordinate |
663 | - y : y coordinate |
664 | - w : width value |
665 | - h : height value |
666 | - |
667 | - <layout> methods can then be passed Shape specific |
668 | - keyword arguments which can be used as draw-time modifiers. |
669 | - """ |
670 | - |
671 | - def __init__(self, direction): |
672 | - self.direction = direction |
673 | - |
674 | - def layout(self, cr, x, y, w, h, *args, **kwargs): |
675 | - if self.direction != Gtk.TextDirection.RTL: |
676 | - self._layout_ltr(cr, x, y, w, h, *args, **kwargs) |
677 | - else: |
678 | - self._layout_rtl(cr, x, y, w, h, *args, **kwargs) |
679 | - |
680 | - |
681 | -class ShapeRoundedRectangle(Shape): |
682 | - |
683 | - """ |
684 | - RoundedRectangle lays out a rectangle with all four corners |
685 | - rounded as specified at the layout call by the keyword argument: |
686 | - |
687 | - radius : an integer or float specifying the corner radius. |
688 | - The radius must be > 0. |
689 | - |
690 | - RoundedRectangle is not direction sensitive. |
691 | - """ |
692 | - |
693 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
694 | - Shape.__init__(self, direction) |
695 | - |
696 | - def layout(self, cr, x, y, w, h, *args, **kwargs): |
697 | - r = kwargs['radius'] |
698 | - |
699 | - cr.new_sub_path() |
700 | - cr.arc(r + x, r + y, r, PI, 270 * PI_OVER_180) |
701 | - cr.arc(w - r, r + y, r, 270 * PI_OVER_180, 0) |
702 | - cr.arc(w - r, h - r, r, 0, 90 * PI_OVER_180) |
703 | - cr.arc(r + x, h - r, r, 90 * PI_OVER_180, PI) |
704 | - cr.close_path() |
705 | - |
706 | - |
707 | -class ShapeRoundedRectangleIrregular(Shape): |
708 | - |
709 | - """ |
710 | - RoundedRectangleIrregular lays out a rectangle for which each |
711 | - individual corner can be rounded by a specific radius, |
712 | - as specified at the layout call by the keyword argument: |
713 | - |
714 | - radii : a 4-tuple of ints or floats specifying the radius for |
715 | - each corner. A value of 0 is acceptable as a radius, it |
716 | - will result in a squared corner. |
717 | - |
718 | - RoundedRectangleIrregular is not direction sensitive. |
719 | - """ |
720 | - |
721 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
722 | - Shape.__init__(self, direction) |
723 | - |
724 | - def layout(self, cr, x, y, w, h, *args, **kwargs): |
725 | - nw, ne, se, sw = kwargs['radii'] |
726 | - |
727 | - cr.save() |
728 | - cr.translate(x, y) |
729 | - if nw: |
730 | - cr.new_sub_path() |
731 | - cr.arc(nw, nw, nw, PI, 270 * PI_OVER_180) |
732 | - else: |
733 | - cr.move_to(0, 0) |
734 | - if ne: |
735 | - cr.arc(w - ne, ne, ne, 270 * PI_OVER_180, 0) |
736 | - else: |
737 | - cr.rel_line_to(w - nw, 0) |
738 | - if se: |
739 | - cr.arc(w - se, h - se, se, 0, 90 * PI_OVER_180) |
740 | - else: |
741 | - cr.rel_line_to(0, h - ne) |
742 | - if sw: |
743 | - cr.arc(sw, h - sw, sw, 90 * PI_OVER_180, PI) |
744 | - else: |
745 | - cr.rel_line_to(-(w - se), 0) |
746 | - |
747 | - cr.close_path() |
748 | - cr.restore() |
749 | - |
750 | - |
751 | -class ShapeStartArrow(Shape): |
752 | - |
753 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
754 | - Shape.__init__(self, direction) |
755 | - |
756 | - def _layout_ltr(self, cr, x, y, w, h, *args, **kwargs): |
757 | - aw = kwargs['arrow_width'] |
758 | - r = kwargs['radius'] |
759 | - |
760 | - cr.new_sub_path() |
761 | - cr.arc(r + x, r + y, r, PI, 270 * PI_OVER_180) |
762 | - # arrow head |
763 | - cr.line_to(w - aw, y) |
764 | - cr.line_to(w - x + 1, (h + y) / 2) |
765 | - cr.line_to(w - aw, h) |
766 | - cr.arc(r + x, h - r, r, 90 * PI_OVER_180, PI) |
767 | - cr.close_path() |
768 | - |
769 | - def _layout_rtl(self, cr, x, y, w, h, *args, **kwargs): |
770 | - aw = kwargs['arrow_width'] |
771 | - r = kwargs['radius'] |
772 | - |
773 | - cr.new_sub_path() |
774 | - cr.move_to(x, (h + y) / 2) |
775 | - cr.line_to(aw, y) |
776 | - cr.arc(w - r, r + y, r, 270 * PI_OVER_180, 0) |
777 | - cr.arc(w - r, h - r, r, 0, 90 * PI_OVER_180) |
778 | - cr.line_to(aw, h) |
779 | - cr.close_path() |
780 | - |
781 | - |
782 | -class ShapeMidArrow(Shape): |
783 | - |
784 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
785 | - Shape.__init__(self, direction) |
786 | - |
787 | - def _layout_ltr(self, cr, x, y, w, h, *args, **kwargs): |
788 | - aw = kwargs['arrow_width'] |
789 | - |
790 | - cr.move_to(x, y) |
791 | - # arrow head |
792 | - cr.line_to(w - aw, y) |
793 | - cr.line_to(w - x + 1, (h + y) / 2) |
794 | - cr.line_to(w - aw, h) |
795 | - cr.line_to(x, h) |
796 | - cr.close_path() |
797 | - |
798 | - def _layout_rtl(self, cr, x, y, w, h, *args, **kwargs): |
799 | - aw = kwargs['arrow_width'] |
800 | - |
801 | - cr.move_to(x, (h + y) / 2) |
802 | - cr.line_to(aw, y) |
803 | - cr.line_to(w, y) |
804 | - cr.line_to(w, h) |
805 | - cr.line_to(aw, h) |
806 | - cr.close_path() |
807 | - |
808 | - |
809 | -class ShapeEndCap(Shape): |
810 | - |
811 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
812 | - Shape.__init__(self, direction) |
813 | - |
814 | - def _layout_ltr(self, cr, x, y, w, h, *args, **kwargs): |
815 | - r = kwargs['radius'] |
816 | - aw = kwargs['arrow_width'] |
817 | - |
818 | - cr.move_to(x - 1, y) |
819 | - cr.arc(w - r, r + y, r, 270 * PI_OVER_180, 0) |
820 | - cr.arc(w - r, h - r, r, 0, 90 * PI_OVER_180) |
821 | - cr.line_to(x - 1, h) |
822 | - cr.line_to(x + aw, (h + y) / 2) |
823 | - cr.close_path() |
824 | - |
825 | - def _layout_rtl(self, cr, x, y, w, h, *args, **kwargs): |
826 | - r = kwargs['radius'] |
827 | - |
828 | - cr.arc(r + x, r + y, r, PI, 270 * PI_OVER_180) |
829 | - cr.line_to(w, y) |
830 | - cr.line_to(w, h) |
831 | - cr.arc(r + x, h - r, r, 90 * PI_OVER_180, PI) |
832 | - cr.close_path() |
833 | - |
834 | - |
835 | -class Circle(Shape): |
836 | - |
837 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
838 | - Shape.__init__(self, direction) |
839 | - |
840 | - @staticmethod |
841 | - def layout(cr, x, y, w, h, *args, **kwargs): |
842 | - cr.new_path() |
843 | - |
844 | - r = min(w, h) * 0.5 |
845 | - x += int((w - 2 * r) / 2) |
846 | - y += int((h - 2 * r) / 2) |
847 | - |
848 | - cr.arc(r + x, r + y, r, 0, 360 * PI_OVER_180) |
849 | - cr.close_path() |
850 | - |
851 | - |
852 | -class ShapeStar(Shape): |
853 | - |
854 | - def __init__(self, points, indent=0.61, direction=Gtk.TextDirection.LTR): |
855 | - self.coords = self._calc_coords(points, 1 - indent) |
856 | - |
857 | - def _calc_coords(self, points, indent): |
858 | - coords = [] |
859 | - step = radian(180.0 / points) |
860 | - |
861 | - for i in range(2 * points): |
862 | - if i % 2: |
863 | - x = (sin(step * i) + 1) * 0.5 |
864 | - y = (cos(step * i) + 1) * 0.5 |
865 | - else: |
866 | - x = (sin(step * i) * indent + 1) * 0.5 |
867 | - y = (cos(step * i) * indent + 1) * 0.5 |
868 | - |
869 | - coords.append((x, y)) |
870 | - return coords |
871 | - |
872 | - def layout(self, cr, x, y, w, h): |
873 | - points = [(sx_sy[0] * w + x, sx_sy[1] * h + y) |
874 | - for sx_sy in self.coords] |
875 | - cr.move_to(*points[0]) |
876 | - |
877 | - for p in points[1:]: |
878 | - cr.line_to(*p) |
879 | - |
880 | - cr.close_path() |
881 | |
882 | === modified file 'softwarecenter/ui/gtk3/views/appdetailsview.py' |
883 | --- softwarecenter/ui/gtk3/views/appdetailsview.py 2012-03-15 21:47:29 +0000 |
884 | +++ softwarecenter/ui/gtk3/views/appdetailsview.py 2012-03-19 22:53:18 +0000 |
885 | @@ -37,7 +37,6 @@ |
886 | from softwarecenter.db.application import Application |
887 | from softwarecenter.db import DebFileApplication |
888 | from softwarecenter.backend.reviews import ReviewStats |
889 | -#from softwarecenter.backend.zeitgeist_simple import zeitgeist_singleton |
890 | from softwarecenter.enums import (AppActions, |
891 | PkgStates, |
892 | Icons, |
893 | @@ -1165,10 +1164,6 @@ |
894 | vb_inner.pack_start(self.title, False, False, 0) |
895 | vb_inner.pack_start(self.subtitle, False, False, 0) |
896 | |
897 | - # usage |
898 | - #~ self.usage = mkit.BubbleLabel() |
899 | - #~ vb_inner.pack_start(self.usage, True, True, 0) |
900 | - |
901 | # star rating box/widget |
902 | self.review_stats_widget = StarRatingsWidget() |
903 | self.review_stats = Gtk.HBox() |
904 | @@ -1539,9 +1534,6 @@ |
905 | if not summary: |
906 | summary = "" |
907 | |
908 | - # hide stuff |
909 | - #~ self.usage.hide() |
910 | - |
911 | # depending on pkg install state set action labels |
912 | self.pkg_statusbar.configure(app_details, app_details.pkg_state) |
913 | |
914 | @@ -1570,9 +1562,6 @@ |
915 | # show where it is |
916 | self._configure_where_is_it() |
917 | |
918 | - # async query zeitgeist and rnr |
919 | - self._update_usage_counter() |
920 | - |
921 | def _update_minimal(self, app_details): |
922 | self._update_app_icon(app_details) |
923 | self._update_pkg_info_table(app_details) |
924 | @@ -2045,35 +2034,6 @@ |
925 | def set_section(self, section): |
926 | self.section = section |
927 | |
928 | - def _update_usage_counter(self): |
929 | - """ try to get the usage counter from zeitgeist """ |
930 | - def _zeitgeist_callback(counter): |
931 | - LOG.debug("zeitgeist usage: %s" % counter) |
932 | - if counter == 0: |
933 | - # this probably means we just have no idea about it, |
934 | - # so instead of saying "Used: never" we just return |
935 | - # this can go away when zeitgeist captures more events |
936 | - # --there are still cases when we really do want to hide this |
937 | - self.usage.hide() |
938 | - return |
939 | - if counter <= 100: |
940 | - label_string = gettext.ngettext("Used: one time", |
941 | - "Used: %(amount)s times", |
942 | - counter) % {'amount': counter} |
943 | - else: |
944 | - label_string = _("Used: over 100 times") |
945 | - self.usage.set_text('<small>%s</small>' % label_string) |
946 | - self.usage.show() |
947 | - |
948 | - # try to get it |
949 | - # FIXME |
950 | - # try: |
951 | - # zeitgeist_singleton.get_usage_counter( |
952 | - # self.app_details.desktop_file, _zeitgeist_callback) |
953 | - # except Exception, e: |
954 | - # LOG.warning("could not update the usage counter: %s " % e) |
955 | - # self.usage.hide() |
956 | - |
957 | |
958 | def get_test_window_appdetails(): |
959 | |
960 | |
961 | === modified file 'softwarecenter/ui/gtk3/widgets/exhibits.py' |
962 | --- softwarecenter/ui/gtk3/widgets/exhibits.py 2012-03-09 12:50:38 +0000 |
963 | +++ softwarecenter/ui/gtk3/widgets/exhibits.py 2012-03-19 22:53:18 +0000 |
964 | @@ -32,7 +32,6 @@ |
965 | |
966 | from softwarecenter.utils import SimpleFileDownloader |
967 | from softwarecenter.ui.gtk3.em import StockEms |
968 | -from softwarecenter.ui.gtk3.shapes import Circle |
969 | from softwarecenter.ui.gtk3.drawing import rounded_rect |
970 | from softwarecenter.ui.gtk3.utils import point_in |
971 | import softwarecenter.paths |
972 | @@ -197,9 +196,20 @@ |
973 | y = (a.height - ds_h) / 2 |
974 | Gdk.cairo_set_source_pixbuf(cr, self._dropshadow, 0, y) |
975 | cr.paint() |
976 | - Circle.layout(cr, self._margin, (a.height - ds_h) / 2 + self._margin, |
977 | - a.width - 2 * self._margin, |
978 | - a.width - 2 * self._margin) |
979 | + |
980 | + # layout circle |
981 | + x = self._margin |
982 | + y = (a.height - ds_h) / 2 + self._margin |
983 | + w = a.width - 2 * self._margin |
984 | + h = a.width - 2 * self._margin |
985 | + cr.new_path() |
986 | + r = min(w, h) * 0.5 |
987 | + x += int((w - 2 * r) / 2) |
988 | + y += int((h - 2 * r) / 2) |
989 | + from math import pi |
990 | + cr.arc(r + x, r + y, r, 0, 2 * pi) |
991 | + cr.close_path() |
992 | + |
993 | if self.is_active: |
994 | color = context.get_background_color(Gtk.StateFlags.SELECTED) |
995 | else: |
996 | |
997 | === modified file 'softwarecenter/ui/gtk3/widgets/stars.py' |
998 | --- softwarecenter/ui/gtk3/widgets/stars.py 2012-03-08 17:42:36 +0000 |
999 | +++ softwarecenter/ui/gtk3/widgets/stars.py 2012-03-19 22:53:18 +0000 |
1000 | @@ -24,7 +24,6 @@ |
1001 | |
1002 | from gi.repository import Gtk, Gdk, GObject |
1003 | |
1004 | -from softwarecenter.ui.gtk3.shapes import ShapeStar |
1005 | from softwarecenter.ui.gtk3.em import StockEms, em, small_em, big_em |
1006 | |
1007 | |
1008 | @@ -50,6 +49,38 @@ |
1009 | REACTIVE = -1 |
1010 | |
1011 | |
1012 | +class ShapeStar(): |
1013 | + def __init__(self, points, indent=0.61): |
1014 | + self.coords = self._calc_coords(points, 1 - indent) |
1015 | + |
1016 | + def _calc_coords(self, points, indent): |
1017 | + coords = [] |
1018 | + |
1019 | + from math import cos, pi, sin |
1020 | + step = pi / points |
1021 | + |
1022 | + for i in range(2 * points): |
1023 | + if i % 2: |
1024 | + x = (sin(step * i) + 1) * 0.5 |
1025 | + y = (cos(step * i) + 1) * 0.5 |
1026 | + else: |
1027 | + x = (sin(step * i) * indent + 1) * 0.5 |
1028 | + y = (cos(step * i) * indent + 1) * 0.5 |
1029 | + |
1030 | + coords.append((x, y)) |
1031 | + return coords |
1032 | + |
1033 | + def layout(self, cr, x, y, w, h): |
1034 | + points = [(sx_sy[0] * w + x, sx_sy[1] * h + y) |
1035 | + for sx_sy in self.coords] |
1036 | + cr.move_to(*points[0]) |
1037 | + |
1038 | + for p in points[1:]: |
1039 | + cr.line_to(*p) |
1040 | + |
1041 | + cr.close_path() |
1042 | + |
1043 | + |
1044 | class StarRenderer(ShapeStar): |
1045 | |
1046 | def __init__(self): |
1047 | |
1048 | === removed file 'softwarecenter/ui/gtk3/widgets/unused__pathbar.py' |
1049 | --- softwarecenter/ui/gtk3/widgets/unused__pathbar.py 2012-03-08 14:09:41 +0000 |
1050 | +++ softwarecenter/ui/gtk3/widgets/unused__pathbar.py 1970-01-01 00:00:00 +0000 |
1051 | @@ -1,1235 +0,0 @@ |
1052 | -from gi.repository import Atk |
1053 | -from gi.repository import Gtk, Gdk |
1054 | -from gi.repository import GObject |
1055 | -from gi.repository import Pango |
1056 | - |
1057 | -from softwarecenter.ui.gtk3.em import em |
1058 | - |
1059 | -from gettext import gettext as _ |
1060 | - |
1061 | -import logging |
1062 | -LOG = logging.getLogger("softwarecenter.view.widgets.NavigationBar") |
1063 | - |
1064 | -# pi constants |
1065 | -from math import pi |
1066 | - |
1067 | -PI = pi |
1068 | -PI_OVER_180 = pi / 180 |
1069 | - |
1070 | - |
1071 | -class Shape: |
1072 | - |
1073 | - """ Base class for a Shape implementation. |
1074 | - |
1075 | - Currently implements a single method <layout> which is called |
1076 | - to layout the shape using cairo paths. It can also store the |
1077 | - 'direction' of the shape which should be on of the Gtk.TEXT_DIR |
1078 | - constants. Default 'direction' is Gtk.TextDirection.LTR. |
1079 | - |
1080 | - When implementing a Shape, there are two options available. |
1081 | - |
1082 | - If the Shape is direction dependent, the Shape MUST |
1083 | - implement <_layout_ltr> and <_layout_rtl> methods. |
1084 | - |
1085 | - If the Shape is not direction dependent, then it simply can |
1086 | - override the <layout> method. |
1087 | - |
1088 | - <layout> methods must take the following as arguments: |
1089 | - |
1090 | - cr : a CairoContext |
1091 | - x : x coordinate |
1092 | - y : y coordinate |
1093 | - w : width value |
1094 | - h : height value |
1095 | - |
1096 | - <layout> methods can then be passed Shape specific |
1097 | - keyword arguments which can be used as paint-time modifiers. |
1098 | - """ |
1099 | - |
1100 | - def __init__(self, direction): |
1101 | - self.direction = direction |
1102 | - self.name = 'Shapeless' |
1103 | - self.hadjustment = 0 |
1104 | - self._color = 1, 0, 0 |
1105 | - |
1106 | - def __eq__(self, other): |
1107 | - return self.name == other.name |
1108 | - |
1109 | - def layout(self, cr, x, y, w, h, r, aw): |
1110 | - if self.direction != Gtk.TextDirection.RTL: |
1111 | - self._layout_ltr(cr, x, y, w, h, r, aw) |
1112 | - else: |
1113 | - self._layout_rtl(cr, x, y, w, h, r, aw) |
1114 | - |
1115 | - |
1116 | -class ShapeRoundedRect(Shape): |
1117 | - |
1118 | - """ |
1119 | - RoundedRect lays out a rectangle with all four corners |
1120 | - rounded as specified at the layout call by the keyword argument: |
1121 | - |
1122 | - radius : an integer or float specifying the corner radius. |
1123 | - The radius must be > 0. |
1124 | - |
1125 | - RoundedRectangle is not direction sensitive. |
1126 | - """ |
1127 | - |
1128 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
1129 | - Shape.__init__(self, direction) |
1130 | - self.name = 'RoundedRect' |
1131 | - |
1132 | - def layout(self, cr, x, y, w, h, r, aw): |
1133 | - cr.new_sub_path() |
1134 | - cr.arc(r + x, r + y, r, PI, 270 * PI_OVER_180) |
1135 | - cr.arc(x + w - r, r + y, r, 270 * PI_OVER_180, 0) |
1136 | - cr.arc(x + w - r, y + h - r, r, 0, 90 * PI_OVER_180) |
1137 | - cr.arc(r + x, y + h - r, r, 90 * PI_OVER_180, PI) |
1138 | - cr.close_path() |
1139 | - |
1140 | - |
1141 | -class ShapeStartArrow(Shape): |
1142 | - |
1143 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
1144 | - Shape.__init__(self, direction) |
1145 | - self.name = 'StartArrow' |
1146 | - |
1147 | - def _layout_ltr(self, cr, x, y, w, h, r, aw): |
1148 | - haw = aw / 2 |
1149 | - |
1150 | - cr.new_sub_path() |
1151 | - cr.arc(r + x, r + y, r, PI, 270 * PI_OVER_180) |
1152 | - |
1153 | - # arrow head |
1154 | - cr.line_to(x + w - haw, y) |
1155 | - cr.line_to(x + w + haw, y + (h / 2)) |
1156 | - cr.line_to(x + w - haw, y + h) |
1157 | - |
1158 | - cr.arc(r + x, y + h - r, r, 90 * PI_OVER_180, PI) |
1159 | - cr.close_path() |
1160 | - |
1161 | - def _layout_rtl(self, cr, x, y, w, h, r, aw): |
1162 | - haw = aw / 2 |
1163 | - |
1164 | - cr.new_sub_path() |
1165 | - cr.move_to(x - haw, (y + h) / 2) |
1166 | - cr.line_to(x + aw - haw, y) |
1167 | - cr.arc(x + w - r, r + y, r, 270 * PI_OVER_180, 0) |
1168 | - cr.arc(x + w - r, y + h - r, r, 0, 90 * PI_OVER_180) |
1169 | - cr.line_to(x + aw - haw, y + h) |
1170 | - cr.close_path() |
1171 | - |
1172 | - |
1173 | -class ShapeMidArrow(Shape): |
1174 | - |
1175 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
1176 | - Shape.__init__(self, direction) |
1177 | - #~ self.draw_xoffset = -2 |
1178 | - self._color = 0, 1, 0 |
1179 | - self.name = 'MidArrow' |
1180 | - |
1181 | - def _layout_ltr(self, cr, x, y, w, h, r, aw): |
1182 | - self.hadjustment = haw = aw / 2 |
1183 | - cr.move_to(x - haw - 1, y) |
1184 | - # arrow head |
1185 | - cr.line_to(x + w - haw, y) |
1186 | - cr.line_to(x + w + haw, y + (h / 2)) |
1187 | - cr.line_to(x + w - haw, y + h) |
1188 | - cr.line_to(x - haw - 1, y + h) |
1189 | - |
1190 | - cr.line_to(x + haw - 1, y + (h / 2)) |
1191 | - |
1192 | - cr.close_path() |
1193 | - |
1194 | - def _layout_rtl(self, cr, x, y, w, h, r, aw): |
1195 | - self.hadjustment = haw = -aw / 2 |
1196 | - |
1197 | - cr.move_to(x + haw, (h + y) / 2) |
1198 | - cr.line_to(x + aw + haw, y) |
1199 | - cr.line_to(x + w - haw + 1, y) |
1200 | - cr.line_to(x + w - aw - haw + 1, (y + h) / 2) |
1201 | - cr.line_to(x + w - haw + 1, y + h) |
1202 | - cr.line_to(x + aw + haw, y + h) |
1203 | - cr.close_path() |
1204 | - |
1205 | - |
1206 | -class ShapeEndCap(Shape): |
1207 | - |
1208 | - def __init__(self, direction=Gtk.TextDirection.LTR): |
1209 | - Shape.__init__(self, direction) |
1210 | - #~ self.draw_xoffset = -2 |
1211 | - self._color = 0, 0, 1 |
1212 | - self.name = 'EndCap' |
1213 | - |
1214 | - def _layout_ltr(self, cr, x, y, w, h, r, aw): |
1215 | - self.hadjustment = haw = aw / 2 |
1216 | - |
1217 | - cr.move_to(x - haw - 1, y) |
1218 | - # rounded end |
1219 | - cr.arc(x + w - r, r + y, r, 270 * PI_OVER_180, 0) |
1220 | - cr.arc(x + w - r, y + h - r, r, 0, 90 * PI_OVER_180) |
1221 | - # arrow |
1222 | - cr.line_to(x - haw - 1, y + h) |
1223 | - cr.line_to(x + haw - 1, y + (h / 2)) |
1224 | - cr.close_path() |
1225 | - |
1226 | - def _layout_rtl(self, cr, x, y, w, h, r, aw): |
1227 | - self.hadjustment = haw = -aw / 2 |
1228 | - |
1229 | - cr.arc(r + x, r + y, r, PI, 270 * PI_OVER_180) |
1230 | - cr.line_to(x + w - haw + 1, y) |
1231 | - cr.line_to(x + w - haw - aw + 1, (y + h) / 2) |
1232 | - cr.line_to(x + w - haw + 1, y + h) |
1233 | - cr.arc(r + x, y + h - r, r, 90 * PI_OVER_180, PI) |
1234 | - cr.close_path() |
1235 | - |
1236 | - |
1237 | -class AnimationClock(GObject.GObject): |
1238 | - _1SECOND = 1000 |
1239 | - __gsignals__ = { |
1240 | - "animation-frame": (GObject.SignalFlags.RUN_LAST, |
1241 | - None, |
1242 | - (float,),), |
1243 | - |
1244 | - "animation-finished": (GObject.SignalFlags.RUN_FIRST, |
1245 | - None, |
1246 | - (bool,),), |
1247 | - } |
1248 | - |
1249 | - def __init__(self, fps, duration): |
1250 | - GObject.GObject.__init__(self) |
1251 | - |
1252 | - self.fps = fps |
1253 | - self.in_progress = False |
1254 | - self.set_duration(duration) |
1255 | - |
1256 | - self._clock = None |
1257 | - self._progress = 0 # progress as an msec offset |
1258 | - |
1259 | - def _get_timstep(self): |
1260 | - d = self.duration |
1261 | - return max(10, int(d / ((d / AnimationClock._1SECOND) * self.fps))) |
1262 | - |
1263 | - def _schedule_animation_frame(self): |
1264 | - if self._progress > self.duration: |
1265 | - self._clock = None |
1266 | - self.in_progress = False |
1267 | - self.emit('animation-finished', False) |
1268 | - return False |
1269 | - |
1270 | - self._progress += self._timestep |
1271 | - self.emit('animation-frame', self.progress) |
1272 | - return True |
1273 | - |
1274 | - @property |
1275 | - def progress(self): |
1276 | - return min(1.0, self._progress / self.duration) |
1277 | - |
1278 | - def set_duration(self, duration): |
1279 | - self.duration = float(duration) |
1280 | - self._timestep = self._get_timstep() |
1281 | - |
1282 | - def stop(self, who_called='?'): |
1283 | - |
1284 | - if self._clock: |
1285 | - #~ print who_called+'.Stop' |
1286 | - GObject.source_remove(self._clock) |
1287 | - self.emit('animation-finished', True) |
1288 | - |
1289 | - self._clock = None |
1290 | - self._progress = 0 |
1291 | - self.in_progress = False |
1292 | - |
1293 | - def start(self): |
1294 | - self.stop(who_called='start') |
1295 | - if not self.sequence: |
1296 | - return |
1297 | - |
1298 | - self._clock = GObject.timeout_add(self._timestep, |
1299 | - self._schedule_animation_frame, |
1300 | - priority=100) |
1301 | - self.in_progress = True |
1302 | - |
1303 | - |
1304 | -class PathBarAnimator(AnimationClock): |
1305 | - # animation display constants |
1306 | - FPS = 50 |
1307 | - DURATION = 150 # spec says 150ms |
1308 | - |
1309 | - # animation modes |
1310 | - NONE = 'animation-none' |
1311 | - OUT = 'animation-out' |
1312 | - IN = 'animation-in' |
1313 | - WIDTH_CHANGE = 'animation-width-change' |
1314 | - |
1315 | - def __init__(self, pathbar): |
1316 | - AnimationClock.__init__(self, self.FPS, self.DURATION) |
1317 | - |
1318 | - self.pathbar = pathbar |
1319 | - self.sequence = [] |
1320 | - |
1321 | - self.connect('animation-frame', self._on_animation_frame) |
1322 | - self.connect('animation-finished', self._on_animation_finished) |
1323 | - |
1324 | - def _animate_out(self, part, progress, kwargs): |
1325 | - real_alloc = part.get_allocation() |
1326 | - xo = real_alloc.width - int(real_alloc.width * progress) |
1327 | - |
1328 | - if self.pathbar.get_direction() == Gtk.TextDirection.RTL: |
1329 | - xo *= -1 |
1330 | - |
1331 | - anim_alloc = Gdk.Rectangle() |
1332 | - anim_alloc.x = real_alloc.x - xo |
1333 | - anim_alloc.y = real_alloc.y |
1334 | - anim_alloc.width = real_alloc.width |
1335 | - anim_alloc.height = real_alloc.height |
1336 | - |
1337 | - part.new_frame(anim_alloc) |
1338 | - |
1339 | - def _animate_in(self, part, progress, kwargs): |
1340 | - real_alloc = part.get_allocation() |
1341 | - xo = int(real_alloc.width * progress) |
1342 | - |
1343 | - if self.pathbar.get_direction() == Gtk.TextDirection.RTL: |
1344 | - xo *= -1 |
1345 | - |
1346 | - anim_alloc = Gdk.Rectangle() |
1347 | - anim_alloc.x = real_alloc.x - xo |
1348 | - anim_alloc.y = real_alloc.y |
1349 | - anim_alloc.width = real_alloc.width |
1350 | - anim_alloc.height = real_alloc.height |
1351 | - |
1352 | - part.new_frame(anim_alloc) |
1353 | - |
1354 | - def _animate_width_change(self, part, progress, kwargs): |
1355 | - start_w = kwargs['start_width'] |
1356 | - end_w = kwargs['end_width'] |
1357 | - |
1358 | - width = int(round(start_w + (end_w - start_w) * progress)) |
1359 | - part.set_size_request(width, part.get_height_request()) |
1360 | - |
1361 | - def _on_animation_frame(self, clock, progress): |
1362 | - if not self.sequence: |
1363 | - return |
1364 | - |
1365 | - for actor, animation, kwargs in self.sequence: |
1366 | - if animation == PathBarAnimator.NONE: |
1367 | - continue |
1368 | - |
1369 | - if animation == PathBarAnimator.OUT: |
1370 | - self._animate_out(actor, progress, kwargs) |
1371 | - |
1372 | - elif animation == PathBarAnimator.IN: |
1373 | - self._animate_in(actor, progress, kwargs) |
1374 | - |
1375 | - elif animation == PathBarAnimator.WIDTH_CHANGE: |
1376 | - self._animate_width_change(actor, progress, kwargs) |
1377 | - |
1378 | - def _on_animation_finished(self, clock, interrupted): |
1379 | - for actor, animation, kwargs in self.sequence: |
1380 | - actor.animation_finished() |
1381 | - |
1382 | - self.sequence = [] |
1383 | - self.pathbar.psuedo_parts = [] |
1384 | - self.pathbar.queue_draw() |
1385 | - |
1386 | - def append_animation(self, actor, animation, **kwargs): |
1387 | - self.sequence.append((actor, animation, kwargs)) |
1388 | - |
1389 | - def reset(self, who_called='?'): |
1390 | - AnimationClock.stop(self, who_called=who_called + '.reset') |
1391 | - self.sequence = [] |
1392 | - |
1393 | - |
1394 | -class PathBar(Gtk.HBox): |
1395 | - |
1396 | - MIN_PART_WIDTH = 25 # pixels |
1397 | - |
1398 | - def __init__(self): |
1399 | - GObject.GObject.__init__(self) |
1400 | - self.set_redraw_on_allocate(False) |
1401 | - self.set_size_request(-1, em(1.75)) |
1402 | - self._allocation = None |
1403 | - |
1404 | - # Accessibility info |
1405 | - atk_desc = self.get_accessible() |
1406 | - atk_desc.set_name(_("You are here:")) |
1407 | - atk_desc.set_role(Atk.Role.PANEL) |
1408 | - |
1409 | - self.use_animations = True |
1410 | - self.animator = PathBarAnimator(self) |
1411 | - |
1412 | - self.out_of_width = False |
1413 | - self.psuedo_parts = [] |
1414 | - |
1415 | - # used for certain button press logic |
1416 | - self._press_origin = None |
1417 | - # tracks the id of the revealer timeout |
1418 | - self._revealer = None |
1419 | - |
1420 | - # values derived from the gtk settings |
1421 | - s = Gtk.Settings.get_default() |
1422 | - # time to wait before revealing a part on enter event in ms |
1423 | - self._timeout_reveal = s.get_property("gtk-tooltip-timeout") |
1424 | - # time to wait until emitting click event in ms |
1425 | - self._timeout_initial = s.get_property("gtk-timeout-initial") |
1426 | - |
1427 | - # les signales! |
1428 | - self.connect('size-allocate', self._on_allocate) |
1429 | - self.connect('draw', self._on_draw) |
1430 | - |
1431 | - # sugar |
1432 | - def __len__(self): |
1433 | - return len(self.get_children()) |
1434 | - |
1435 | - def __getitem__(self, index): |
1436 | - return self.get_children()[index] |
1437 | - |
1438 | - # signal handlers |
1439 | - def _on_allocate(self, widget, _): |
1440 | - allocation = self.get_allocation() |
1441 | - |
1442 | - if self._allocation == allocation: |
1443 | - return True |
1444 | - |
1445 | - # prevent vertical bobby when the searchentry is shown/hidden |
1446 | - if allocation.height > self.get_property('height-request'): |
1447 | - self.set_property('height-request', allocation.height) |
1448 | - |
1449 | - if not self._allocation: |
1450 | - self._allocation = allocation |
1451 | - self.queue_draw() |
1452 | - return True |
1453 | - |
1454 | - pthbr_width = allocation.width |
1455 | - parts_width = self.get_parts_width() |
1456 | - |
1457 | - #~ print parts_width, pthbr_width |
1458 | - |
1459 | - #~ self.animator.reset('on_allocate') |
1460 | - self.set_use_animations(True) |
1461 | - |
1462 | - if pthbr_width > parts_width and self.out_of_width: |
1463 | - dw = pthbr_width - parts_width |
1464 | - self._grow_parts(dw) |
1465 | - |
1466 | - elif pthbr_width < parts_width: |
1467 | - overhang = parts_width - pthbr_width |
1468 | - if overhang > 0: |
1469 | - self.set_use_animations(False) |
1470 | - self._shrink_parts(overhang) |
1471 | - |
1472 | - self._allocation = allocation |
1473 | - if self.use_animations and self.animator.sequence and not \ |
1474 | - self.animator.in_progress: |
1475 | - self.animator.start() |
1476 | - else: |
1477 | - self.queue_draw() |
1478 | - |
1479 | - def _on_draw(self, widget, cr): |
1480 | - # always paint psuedo parts first |
1481 | - a = self.get_allocation() |
1482 | - context = self.get_style_context() |
1483 | - context.save() |
1484 | - context.add_class("button") |
1485 | - |
1486 | - self._paint_psuedo_parts(cr, context, a.x, a.y) |
1487 | - |
1488 | - # paint a frame around the entire pathbar |
1489 | - width = self.get_parts_width() |
1490 | - Gtk.render_background(context, cr, 1, 1, width - 2, a.height - 2) |
1491 | - |
1492 | - self._paint_widget_parts(cr, context, a.x, a.y) |
1493 | - |
1494 | - Gtk.render_frame(context, cr, 0, 0, width, a.height) |
1495 | - context.restore() |
1496 | - return True |
1497 | - |
1498 | - # private methods |
1499 | - def _paint_widget_parts(self, cr, context, xo, yo): |
1500 | - parts = self.get_children() |
1501 | - # paint in reverse order, so we get correct overlapping during |
1502 | - # animation |
1503 | - parts.reverse() |
1504 | - for part in parts: |
1505 | - part.paint(cr, |
1506 | - part.animation_allocation or part.get_allocation(), |
1507 | - context, |
1508 | - xo, yo) |
1509 | - |
1510 | - def _paint_psuedo_parts(self, cr, context, xo, yo): |
1511 | - # a special case: paint psuedo parts paint first, |
1512 | - # i.e those parts animating 'in' on their removal |
1513 | - for part in self.psuedo_parts: |
1514 | - part.paint(cr, |
1515 | - part.animation_allocation or part.get_allocation(), |
1516 | - context, |
1517 | - xo, yo) |
1518 | - |
1519 | - def _shrink_parts(self, overhang): |
1520 | - self.out_of_width = True |
1521 | - |
1522 | - for part in self: |
1523 | - old_width = part.get_width_request() |
1524 | - new_width = max(self.MIN_PART_WIDTH, old_width - overhang) |
1525 | - |
1526 | - if False: # self.use_animations: |
1527 | - self.animator.append_animation(part, |
1528 | - PathBarAnimator.WIDTH_CHANGE, |
1529 | - start_width=old_width, |
1530 | - end_width=new_width) |
1531 | - else: |
1532 | - part.set_size_request(new_width, |
1533 | - part.get_height_request()) |
1534 | - |
1535 | - overhang -= old_width - new_width |
1536 | - if overhang <= 0: |
1537 | - break |
1538 | - |
1539 | - def _grow_parts(self, claim): |
1540 | - children = self.get_children() |
1541 | - children.reverse() |
1542 | - |
1543 | - for part in children: |
1544 | - |
1545 | - if part.get_allocation().width == part.get_natural_width(): |
1546 | - continue |
1547 | - |
1548 | - growth = min(claim, (part.get_natural_width() - part.width)) |
1549 | - if growth <= 0: |
1550 | - break |
1551 | - |
1552 | - claim -= growth |
1553 | - |
1554 | - if self.use_animations: |
1555 | - self.animator.append_animation(part, |
1556 | - PathBarAnimator.WIDTH_CHANGE, |
1557 | - start_width=part.width, |
1558 | - end_width=part.width + growth) |
1559 | - else: |
1560 | - part.set_size_request(part.width + growth, |
1561 | - part.get_height_request()) |
1562 | - |
1563 | - def _make_space(self, part): |
1564 | - children = self.get_children() |
1565 | - if not children: |
1566 | - return |
1567 | - |
1568 | - cur_width = self.get_parts_width() |
1569 | - incomming_width = cur_width + part.get_width_request() |
1570 | - overhang = incomming_width - self.get_allocation().width |
1571 | - |
1572 | - if overhang > 0: |
1573 | - print 'shrink parts by:', overhang |
1574 | - self._shrink_parts(overhang) |
1575 | - |
1576 | - def _reclaim_space(self, part): |
1577 | - if not self.out_of_width: |
1578 | - return |
1579 | - |
1580 | - claim = part.get_width_request() |
1581 | - self._grow_parts(claim) |
1582 | - |
1583 | - def _append_compose_parts(self, new_part): |
1584 | - d = self.get_direction() |
1585 | - children = self.get_children() |
1586 | - n_parts = len(children) |
1587 | - |
1588 | - if n_parts > 0: |
1589 | - new_part.set_shape(ShapeEndCap(d)) |
1590 | - first_part = children[0] |
1591 | - first_part.set_shape(ShapeStartArrow(d)) |
1592 | - else: |
1593 | - new_part.set_shape(ShapeRoundedRect(d)) |
1594 | - |
1595 | - if not n_parts > 1: |
1596 | - return |
1597 | - |
1598 | - new_mid = children[-1] |
1599 | - new_mid.set_shape(ShapeMidArrow(d)) |
1600 | - |
1601 | - def _remove_compose_parts(self): |
1602 | - d = self.get_direction() |
1603 | - children = self.get_children() |
1604 | - n_parts = len(children) |
1605 | - |
1606 | - if n_parts == 0: |
1607 | - return |
1608 | - |
1609 | - elif n_parts == 1: |
1610 | - children[0].set_shape(ShapeRoundedRect(d)) |
1611 | - return |
1612 | - |
1613 | - last = children[-1] |
1614 | - last.set_shape(ShapeEndCap(d)) |
1615 | - self.queue_draw() |
1616 | - |
1617 | - def _cleanup_revealer(self): |
1618 | - if not self._revealer: |
1619 | - return |
1620 | - GObject.source_remove(self._revealer) |
1621 | - self._revealer = None |
1622 | - |
1623 | - def _theme(self, part): |
1624 | - #~ part.set_padding(self.theme['xpad'], self.theme['ypad']) |
1625 | - part.set_padding(12, 4) |
1626 | - |
1627 | - # public methods |
1628 | - @property |
1629 | - def first_part(self): |
1630 | - children = self.get_children() |
1631 | - if children: |
1632 | - return children[0] |
1633 | - |
1634 | - @property |
1635 | - def last_part(self): |
1636 | - children = self.get_children() |
1637 | - if children: |
1638 | - return children[-1] |
1639 | - |
1640 | - def reveal_part(self, part, animate=True): |
1641 | - # do not do here: |
1642 | - #~ self.animator.reset(who_called='reveal_animation') |
1643 | - self.set_use_animations(animate) |
1644 | - |
1645 | - part_old_width = part.get_width_request() |
1646 | - part_new_width = part.get_natural_width() |
1647 | - |
1648 | - if part_new_width == part_old_width: |
1649 | - return |
1650 | - |
1651 | - change_amount = part_new_width - part_old_width |
1652 | - |
1653 | - for p in self.get_children(): |
1654 | - |
1655 | - if p == part: |
1656 | - old_width = part_old_width |
1657 | - new_width = part_new_width |
1658 | - else: |
1659 | - if change_amount <= 0: |
1660 | - continue |
1661 | - |
1662 | - old_width = p.get_width_request() |
1663 | - new_width = max(self.MIN_PART_WIDTH, old_width - change_amount) |
1664 | - change_amount -= old_width - new_width |
1665 | - |
1666 | - if self.use_animations: |
1667 | - self.animator.append_animation(p, |
1668 | - PathBarAnimator.WIDTH_CHANGE, |
1669 | - start_width=old_width, |
1670 | - end_width=new_width) |
1671 | - else: |
1672 | - p.set_size_request(new_width, |
1673 | - p.get_height_request()) |
1674 | - |
1675 | - self.animator.start() |
1676 | - |
1677 | - def queue_reveal_part(self, part): |
1678 | - |
1679 | - def reveal_part_cb(part): |
1680 | - self.reveal_part(part) |
1681 | - return |
1682 | - |
1683 | - self._cleanup_revealer() |
1684 | - self._revealer = GObject.timeout_add(self._timeout_reveal, |
1685 | - reveal_part_cb, |
1686 | - part) |
1687 | - |
1688 | - def get_parts_width(self): |
1689 | - last = self.last_part |
1690 | - if not last: |
1691 | - return 0 |
1692 | - |
1693 | - if self.get_direction() != Gtk.TextDirection.RTL: |
1694 | - return last.x + last.width - self.first_part.x |
1695 | - |
1696 | - first = self.first_part |
1697 | - return first.x + first.width - last.x |
1698 | - |
1699 | - def get_visual_width(self): |
1700 | - last = self.last_part |
1701 | - first = self.first_part |
1702 | - if not last: |
1703 | - return 0 |
1704 | - |
1705 | - la = last.animation_allocation or last.get_allocation() |
1706 | - fa = first.animation_allocation or first.get_allocation() |
1707 | - |
1708 | - if self.get_direction() != Gtk.TextDirection.RTL: |
1709 | - return la.x + la.width - fa.x |
1710 | - |
1711 | - return fa.x + fa.width - la.x |
1712 | - |
1713 | - def set_use_animations(self, use_animations): |
1714 | - self.use_animations = use_animations |
1715 | - if not use_animations and self.animator.in_progress: |
1716 | - self.animator.reset() |
1717 | - |
1718 | - def append(self, part): |
1719 | - print 'append', part |
1720 | |
1721 | - part.set_nopaint(True) |
1722 | - self.animator.reset('append') |
1723 | - |
1724 | - self._theme(part) |
1725 | - self._append_compose_parts(part) |
1726 | - self._make_space(part) |
1727 | - |
1728 | - self.pack_start(part, False, False, 0) |
1729 | - part.show() |
1730 | - |
1731 | - if self.use_animations: |
1732 | - # XXX: please note that animations also get queued up |
1733 | - # within _shrink_parts() |
1734 | - self.animator.append_animation(part, PathBarAnimator.OUT) |
1735 | - #~ print self.animator.sequence |
1736 | - self.animator.start() |
1737 | - else: |
1738 | - part.set_nopaint(False) |
1739 | - part.queue_draw() |
1740 | - |
1741 | - def pop(self): |
1742 | - children = self.get_children() |
1743 | - if not children: |
1744 | - return |
1745 | - |
1746 | - self.animator.reset('pop') |
1747 | - |
1748 | - last = children[-1] |
1749 | - if self.use_animations: |
1750 | - # because we remove the real part immediately we need to |
1751 | - # replicate just enough attributes to preform the slide in |
1752 | - # animation |
1753 | - part = PsuedoPathPart(self, last) |
1754 | - self.psuedo_parts.append(part) |
1755 | - |
1756 | - self.remove(last) |
1757 | - |
1758 | - self._remove_compose_parts() |
1759 | - self._reclaim_space(last) |
1760 | - |
1761 | - last.destroy() |
1762 | - |
1763 | - if not self.use_animations: |
1764 | - return |
1765 | - |
1766 | - self.animator.append_animation(part, PathBarAnimator.IN) |
1767 | - self.animator.start() |
1768 | - |
1769 | - def navigate_up(self): |
1770 | - """ just another name for pop() """ |
1771 | - self.pop() |
1772 | - |
1773 | - |
1774 | -class PathPartCommon: |
1775 | - |
1776 | - def __init__(self): |
1777 | - self.animation_in_progress = False |
1778 | - self.animation_allocation = None |
1779 | - |
1780 | - @property |
1781 | - def x(self): |
1782 | - return self.get_allocation().x |
1783 | - |
1784 | - @property |
1785 | - def y(self): |
1786 | - return self.get_allocation().y |
1787 | - |
1788 | - @property |
1789 | - def width(self): |
1790 | - return self.get_allocation().width |
1791 | - |
1792 | - @property |
1793 | - def height(self): |
1794 | - return self.get_allocation().height |
1795 | - |
1796 | - def new_frame(self, allocation): |
1797 | - if self.is_nopaint: |
1798 | - self.is_nopaint = False |
1799 | - if not self.animation_in_progress: |
1800 | - self.animation_in_progress = True |
1801 | - |
1802 | - self.animation_allocation = allocation |
1803 | - self.queue_draw() |
1804 | - |
1805 | - def animation_finished(self): |
1806 | - self.animation_in_progress = False |
1807 | - self.animation_allocation = None |
1808 | - if self.get_parent(): |
1809 | - self.get_parent().queue_draw() |
1810 | - |
1811 | - def paint(self, cr, a, context, xo, yo): |
1812 | - if self.is_nopaint: |
1813 | - return |
1814 | - |
1815 | - cr.save() |
1816 | - |
1817 | - x, y = 0, 0 |
1818 | - w, h = a.width, a.height |
1819 | - arrow_width = 12 # theme['arrow-width'] |
1820 | - |
1821 | - if isinstance(self, PathPart): |
1822 | - _a = self.get_allocation() |
1823 | - self.shape.layout(cr, |
1824 | - _a.x - xo + 1, _a.y - yo, |
1825 | - w, h, 3, arrow_width) |
1826 | - cr.clip() |
1827 | - else: |
1828 | - Gtk.render_background(context, cr, |
1829 | - a.x - xo - 10, a.y - yo, |
1830 | - a.width + 10, a.height) |
1831 | - |
1832 | - cr.translate(a.x - xo, a.y - yo) |
1833 | - |
1834 | - if self.shape.name.find('Arrow') != -1: |
1835 | - # draw arrow head |
1836 | - cr.move_to(w - arrow_width / 2, 2) |
1837 | - cr.line_to(w + 5, h / 2) |
1838 | - cr.line_to(w - arrow_width / 2, h - 2) |
1839 | - # fetch the line color and stroke |
1840 | - rgba = context.get_border_color(Gtk.StateFlags.NORMAL) |
1841 | - cr.set_source_rgb(rgba.red, rgba.green, rgba.blue) |
1842 | - cr.set_line_width(1) |
1843 | - cr.stroke() |
1844 | - |
1845 | - # render the layout |
1846 | - e = self.layout.get_pixel_extents()[1] |
1847 | - lw, lh = e.width, e.height |
1848 | - pw, ph = a.width, a.height |
1849 | - |
1850 | - x = min(self.xpadding, (pw - lw) / 2) |
1851 | - y = (ph - lh) / 2 |
1852 | - |
1853 | - # layout area |
1854 | - Gtk.render_layout(context, |
1855 | - cr, |
1856 | - int(x), |
1857 | - int(y), |
1858 | - self.layout) |
1859 | - |
1860 | - # paint the focus frame if need be |
1861 | - if isinstance(self, PathPart) and self.has_focus(): |
1862 | - # layout area |
1863 | - x, w, h = x - 2, lw + 4, lh + 1 |
1864 | - Gtk.render_focus(context, cr, x, y, w, h) |
1865 | - |
1866 | - cr.restore() |
1867 | - |
1868 | - |
1869 | -class PsuedoPathPart(PathPartCommon): |
1870 | - |
1871 | - def __init__(self, pathbar, real_part): |
1872 | - PathPartCommon.__init__(self) |
1873 | - self.parent = pathbar |
1874 | - self.style = pathbar.get_style() |
1875 | - self.state = real_part.get_state() |
1876 | - self.allocation = real_part.get_allocation() |
1877 | - self.size_request = real_part.get_size_request() |
1878 | - self.xpadding = real_part.xpadding |
1879 | - self.ypadding = real_part.ypadding |
1880 | - |
1881 | - # PsuedoPathParts are only used during the remove animation |
1882 | - # sequence, so the shape is always a ShapeEndCap |
1883 | - self.shape = ShapeEndCap(pathbar.get_direction()) |
1884 | - |
1885 | - self.label = real_part.label |
1886 | - self.layout = real_part.create_pango_layout(self.label) |
1887 | - |
1888 | - self.is_nopaint = False |
1889 | - |
1890 | - def get_allocation(self): |
1891 | - return self.allocation |
1892 | - |
1893 | - def get_state(self): |
1894 | - return self.state |
1895 | - |
1896 | - def get_width_request(self): |
1897 | - return self.size_request[0] |
1898 | - |
1899 | - def get_height_request(self): |
1900 | - return self.size_request[1] |
1901 | - |
1902 | - def animation_finished(self): |
1903 | - pass |
1904 | - |
1905 | - def queue_draw(self): |
1906 | - a = self.allocation |
1907 | - aw = 12 |
1908 | - self.parent.queue_draw_area(a.x - aw / 2, a.y, |
1909 | - a.width + aw, a.height) |
1910 | - |
1911 | - |
1912 | -class PathPart(Gtk.EventBox, PathPartCommon): |
1913 | - |
1914 | - __gsignals__ = { |
1915 | - "clicked": (GObject.SignalFlags.RUN_LAST, |
1916 | - None, |
1917 | - (),), |
1918 | - } |
1919 | - |
1920 | - def __init__(self, label): |
1921 | - Gtk.EventBox.__init__(self) |
1922 | - PathPartCommon.__init__(self) |
1923 | - self.set_visible_window(False) |
1924 | - |
1925 | - self.atk = self.get_accessible() |
1926 | - self.atk.set_role(Atk.Role.PUSH_BUTTON) |
1927 | - |
1928 | - self.layout = self.create_pango_layout(label) |
1929 | - self.layout.set_ellipsize(Pango.EllipsizeMode.END) |
1930 | - |
1931 | - self.xpadding = 6 |
1932 | - self.ypadding = 3 |
1933 | - |
1934 | - self.shape = ShapeRoundedRect(self.get_direction()) |
1935 | - self.is_nopaint = False |
1936 | - |
1937 | - self.set_label(label) |
1938 | - self._init_event_handling() |
1939 | - |
1940 | - def __repr__(self): |
1941 | - return "PathPart: '%s'" % self.label |
1942 | - |
1943 | - def __str__(self): |
1944 | - return "PathPart: '%s'" % self.label |
1945 | - |
1946 | - # signal handlers |
1947 | - def _on_enter_notify(self, part, event): |
1948 | - self.pathbar.queue_reveal_part(self) |
1949 | - if self.pathbar._press_origin == part: |
1950 | - part.set_state(Gtk.StateFlags.ACTIVE) |
1951 | - else: |
1952 | - part.set_state(Gtk.StateFlags.PRELIGHT) |
1953 | - self.queue_draw() |
1954 | - |
1955 | - def _on_leave_notify(self, part, event): |
1956 | - self.pathbar.queue_reveal_part(self.pathbar.last_part) |
1957 | - part.set_state(Gtk.StateFlags.NORMAL) |
1958 | - self.queue_draw() |
1959 | - |
1960 | - def _on_button_press(self, part, event): |
1961 | - if event.button != 1: |
1962 | - return |
1963 | - self.pathbar._press_origin = part |
1964 | - part.set_state(Gtk.StateFlags.ACTIVE) |
1965 | - self.queue_draw() |
1966 | - |
1967 | - def _on_button_release(self, part, event): |
1968 | - if event.button != 1: |
1969 | - return |
1970 | - |
1971 | - if self.pathbar._press_origin != part: |
1972 | - self.pathbar._press_origin = None |
1973 | - return |
1974 | - |
1975 | - self.pathbar._press_origin = None |
1976 | - |
1977 | - state = part.get_state() |
1978 | - if state == Gtk.StateFlags.ACTIVE: |
1979 | - part.set_state(Gtk.StateFlags.PRELIGHT) |
1980 | - GObject.timeout_add(self.pathbar._timeout_initial, |
1981 | - self.emit, 'clicked') |
1982 | - |
1983 | - self.queue_draw() |
1984 | - |
1985 | - def _on_key_press(self, part, event): |
1986 | - if event.keyval in (Gdk.KEY_space, Gdk.KEY_Return, Gdk.KEY_KP_Enter): |
1987 | - part.set_state(Gtk.StateFlags.ACTIVE) |
1988 | - self.queue_draw() |
1989 | - |
1990 | - def _on_key_release(self, part, event): |
1991 | - if event.keyval in (Gdk.KEY_space, Gdk.KEY_Return, Gdk.KEY_KP_Enter): |
1992 | - part.set_state(Gtk.StateFlags.NORMAL) |
1993 | - GObject.timeout_add(self.pathbar._timeout_initial, |
1994 | - self.emit, 'clicked') |
1995 | - self.queue_draw() |
1996 | - |
1997 | - def _on_focus_in(self, part, event): |
1998 | - self.pathbar.reveal_part(self) |
1999 | - |
2000 | - def _on_focus_out(self, part, event): |
2001 | - self.queue_draw() |
2002 | - |
2003 | - # private methods |
2004 | - def _init_event_handling(self): |
2005 | - self.set_property("can-focus", True) |
2006 | - self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | |
2007 | - Gdk.EventMask.BUTTON_RELEASE_MASK | |
2008 | - Gdk.EventMask.KEY_RELEASE_MASK | |
2009 | - Gdk.EventMask.KEY_PRESS_MASK | |
2010 | - Gdk.EventMask.ENTER_NOTIFY_MASK | |
2011 | - Gdk.EventMask.LEAVE_NOTIFY_MASK) |
2012 | - |
2013 | - self.connect("enter-notify-event", self._on_enter_notify) |
2014 | - self.connect("leave-notify-event", self._on_leave_notify) |
2015 | - self.connect("button-press-event", self._on_button_press) |
2016 | - self.connect("button-release-event", self._on_button_release) |
2017 | - self.connect("key-press-event", self._on_key_press) |
2018 | - self.connect("key-release-event", self._on_key_release) |
2019 | - self.connect("focus-in-event", self._on_focus_in) |
2020 | - self.connect("focus-out-event", self._on_focus_out) |
2021 | - |
2022 | - def _calc_natural_size(self, who_called='?'): |
2023 | - ne = self.natural_extents |
2024 | - nw, nh = ne.width, ne.height |
2025 | - |
2026 | - nw += self.shape.hadjustment + 2 * self.xpadding |
2027 | - nh += 2 * self.ypadding |
2028 | - |
2029 | - self.natural_size = nw, nh |
2030 | - self.set_size_request(nw, nh) |
2031 | - |
2032 | - # public methods |
2033 | - @property |
2034 | - def pathbar(self): |
2035 | - return self.get_parent() |
2036 | - |
2037 | - def set_padding(self, xpadding, ypadding): |
2038 | - self.xpadding = xpadding |
2039 | - self.ypadding = ypadding |
2040 | - self._calc_natural_size() |
2041 | - |
2042 | - def set_size_request(self, width, height): |
2043 | - width = max(2 * self.xpadding + 1, width) |
2044 | - height = max(2 * self.ypadding + 1, height) |
2045 | - self.layout.set_width(Pango.SCALE * (width - 2 * self.xpadding)) |
2046 | - Gtk.Widget.set_size_request(self, width, height) |
2047 | - |
2048 | - def set_nopaint(self, is_nopaint): |
2049 | - self.is_nopaint = is_nopaint |
2050 | - self.queue_draw() |
2051 | - |
2052 | - def set_shape(self, shape): |
2053 | - if shape == self.shape: |
2054 | - return |
2055 | - self.shape = shape |
2056 | - self._calc_natural_size() |
2057 | - self.queue_draw() |
2058 | - |
2059 | - def set_label(self, label): |
2060 | - self.label = label |
2061 | - |
2062 | - self.atk.set_name(label) |
2063 | - self.atk.set_description(_('Navigates to the %s page.') % label) |
2064 | - |
2065 | - self.layout.set_markup(label, -1) |
2066 | - self.layout.set_width(-1) |
2067 | - self.natural_extents = self.layout.get_pixel_extents()[1] |
2068 | - |
2069 | - self._calc_natural_size() |
2070 | - self.queue_draw() |
2071 | - |
2072 | - def get_natural_size(self): |
2073 | - return self.natural_size |
2074 | - |
2075 | - def get_natural_width(self): |
2076 | - return self.natural_size[0] |
2077 | - |
2078 | - def get_natural_height(self): |
2079 | - return self.natural_size[1] |
2080 | - |
2081 | - def get_width_request(self): |
2082 | - return self.get_property("width-request") |
2083 | - |
2084 | - def get_height_request(self): |
2085 | - return self.get_property("height-request") |
2086 | - |
2087 | - def queue_draw(self): |
2088 | - a = self.get_allocation() |
2089 | - parent = self.get_parent() |
2090 | - if parent: |
2091 | - aw = 12 |
2092 | - else: |
2093 | - aw = 0 |
2094 | - self.queue_draw_area(a.x - aw / 2, a.y, |
2095 | - a.width + aw, a.height) |
2096 | - |
2097 | - |
2098 | -class NavigationBar(PathBar): |
2099 | - |
2100 | - def __init__(self, group=None): |
2101 | - PathBar.__init__(self) |
2102 | - self.id_to_part = {} |
2103 | - self._callback_id = None |
2104 | - |
2105 | - def _on_part_clicked(self, part): |
2106 | - part.callback(self, part) |
2107 | - |
2108 | - def add_with_id(self, label, callback, id, do_callback=True, animate=True): |
2109 | - """ |
2110 | - Add a new button with the given label/callback |
2111 | - |
2112 | - If there is the same id already, replace the existing one |
2113 | - with the new one |
2114 | - """ |
2115 | - LOG.debug("add_with_id label='%s' callback='%s' id='%s' " |
2116 | - "do_callback=%s animate=%s" % (label, callback, id, |
2117 | - do_callback, animate)) |
2118 | - |
2119 | - label = GObject.markup_escape_text(label) |
2120 | - |
2121 | - if not self.id_to_part: |
2122 | - self.set_use_animations(False) |
2123 | - else: |
2124 | - self.set_use_animations(animate) |
2125 | - |
2126 | - # check if we have the button of that id or need a new one |
2127 | - if id in self.id_to_part: |
2128 | - part = self.id_to_part[id] |
2129 | - if part.label == label: |
2130 | - return |
2131 | - |
2132 | - part.set_label(label) |
2133 | - else: |
2134 | - part = PathPart(label) |
2135 | - part.connect('clicked', self._on_part_clicked) |
2136 | - |
2137 | - part.set_name(id) |
2138 | - self.id_to_part[id] = part |
2139 | - |
2140 | - part.callback = callback |
2141 | - if do_callback: |
2142 | - # cleanup any superceeded idle callback |
2143 | - if self._callback_id: |
2144 | - GObject.source_remove(self._callback_id) |
2145 | - self._callback_id = None |
2146 | - |
2147 | - # if i do not have call the callback in an idle, |
2148 | - # all hell breaks loose |
2149 | - self._callback_id = GObject.idle_add(callback, |
2150 | - self, # pathbar |
2151 | - part) |
2152 | - |
2153 | - self.append(part) |
2154 | - |
2155 | - def remove_ids(self, *ids, **kwargs): |
2156 | - parts = self.get_parts() |
2157 | - |
2158 | - print 'remove ids', ids |
2159 | - |
2160 | - # it would seem parts can become stale within the id_to_part dict, |
2161 | - # so we clean these up ... |
2162 | - cleanup_ids = [] |
2163 | - # the index of the first part to be clipped |
2164 | - index = len(parts) |
2165 | - |
2166 | - for id, part in self.id_to_part.iteritems(): |
2167 | - if id not in ids: |
2168 | - continue |
2169 | - if part not in parts: |
2170 | - cleanup_ids.append(id) |
2171 | - part.destroy() |
2172 | - else: |
2173 | - index = min(index, parts.index(part)) |
2174 | - |
2175 | - if index == len(parts): |
2176 | - return |
2177 | - |
2178 | - # cleanup any stale id:part pairs in the id_to_part dict |
2179 | - for id in cleanup_ids: |
2180 | - del self.id_to_part[id] |
2181 | - |
2182 | - # remove id:part pairs from the id_to_part dict, for whom removal |
2183 | - # has been requested |
2184 | - for id in ids: |
2185 | - if id in self.id_to_part: |
2186 | - del self.id_to_part[id] |
2187 | - |
2188 | - #~ print index, self.id_to_part.keys() |
2189 | - |
2190 | - # the index is used to remove all parts after the index but we |
2191 | - # keep one part around to animate its removal |
2192 | - for part in parts[index + 1:]: |
2193 | - part.destroy() |
2194 | - |
2195 | - animate = True |
2196 | - if 'animate' in kwargs: |
2197 | - animate = kwargs['animate'] |
2198 | - |
2199 | - # animate the removal of the final part, or not |
2200 | - self.set_use_animations(animate) |
2201 | - self.pop() |
2202 | - |
2203 | - # check if we should call the new tail parts callback |
2204 | - if 'do_callback' in kwargs and kwargs['do_callback']: |
2205 | - part = self[-1] |
2206 | - part.callback(self, part) |
2207 | - |
2208 | - def remove_all(self, **kwargs): |
2209 | - if len(self) <= 1: |
2210 | - return |
2211 | - ids = filter(lambda k: k != 'category', |
2212 | - self.id_to_part.keys()) |
2213 | - self.remove_ids(*ids, **kwargs) |
2214 | - |
2215 | - def has_id(self, id): |
2216 | - return id in self.id_to_part |
2217 | - |
2218 | - def get_parts(self): |
2219 | - return self.get_children() |
2220 | - |
2221 | - def get_active(self): |
2222 | - parts = self.get_parts() |
2223 | - if parts: |
2224 | - return parts[-1] |
2225 | - |
2226 | - def get_button_from_id(self, id): |
2227 | - """ |
2228 | - return the button for the given id (or None) |
2229 | - """ |
2230 | - return self.id_to_part.get(id) |
2231 | - |
2232 | - def set_active_no_callback(self, part): |
2233 | - pass |
2234 | - |
2235 | - |
2236 | -class TestIt: |
2237 | - |
2238 | - def __init__(self): |
2239 | - |
2240 | - def append(button, entry, pathbar): |
2241 | - t = entry.get_text() or 'no label %s' % len(pathbar) |
2242 | - part = PathPart(t) |
2243 | - pathbar.append(part) |
2244 | - |
2245 | - def remove(button, entry, pathbar): |
2246 | - pathbar.pop() |
2247 | - |
2248 | - win = Gtk.Window() |
2249 | - win.set_border_width(30) |
2250 | - win.set_size_request(600, 300) |
2251 | - |
2252 | - vb = Gtk.VBox(spacing=6) |
2253 | - win.add(vb) |
2254 | - |
2255 | - pb = PathBar() |
2256 | - pb.set_size_request(-1, 30) |
2257 | - |
2258 | - vb.pack_start(pb, False, False, 0) |
2259 | - part = PathPart('Get Software') |
2260 | - pb.append(part) |
2261 | - |
2262 | - entry = Gtk.Entry() |
2263 | - vb.pack_start(entry, False, False, 0) |
2264 | - |
2265 | - b = Gtk.Button('Append') |
2266 | - vb.pack_start(b, True, True, 0) |
2267 | - b.connect('clicked', append, entry, pb) |
2268 | - |
2269 | - b = Gtk.Button('Remove') |
2270 | - vb.pack_start(b, True, True, 0) |
2271 | - b.connect('clicked', remove, entry, pb) |
2272 | - |
2273 | - win.show_all() |
2274 | - |
2275 | - win.connect('destroy', Gtk.main_quit) |
2276 | - self.win = win |
2277 | - self.win.pb = pb |
2278 | - |
2279 | - |
2280 | -def get_test_pathbar_window(): |
2281 | - t = TestIt() |
2282 | - return t.win |
2283 | - |
2284 | -if __name__ == '__main__': |
2285 | - win = get_test_pathbar_window() |
2286 | - Gtk.main() |
Thanks, looks fantastic!