diff --git a/rigo/rigo/ui/gtk3/controllers/__init__.py b/rigo/rigo/ui/gtk3/controllers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/rigo/rigo/ui/gtk3/controllers/application.py b/rigo/rigo/ui/gtk3/controllers/application.py
new file mode 100644
index 000000000..15afdc7c3
--- /dev/null
+++ b/rigo/rigo/ui/gtk3/controllers/application.py
@@ -0,0 +1,920 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2012 Fabio Erculiani
+
+Authors:
+ Fabio Erculiani
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+details.
+
+You should have received a copy of the GNU General Public License along with
+this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+
+from gi.repository import Gtk, Gdk, GLib, GObject
+
+from rigo.enums import AppActions
+from rigo.ui.gtk3.widgets.notifications import NotificationBox, \
+ LoginNotificationBox
+from rigo.ui.gtk3.widgets.stars import ReactiveStar
+from rigo.ui.gtk3.widgets.comments import CommentBox
+from rigo.ui.gtk3.widgets.images import ImageBox
+from rigo.utils import build_application_store_url, \
+ escape_markup, prepare_markup
+
+from entropy.const import etpConst, const_debug_write, \
+ const_debug_enabled, const_convert_to_unicode, const_isunicode
+from entropy.misc import ParallelTask
+from entropy.i18n import _
+
+import entropy.tools
+
+class ApplicationViewController(GObject.Object):
+ """
+ Applications View Container, exposing all the events
+ that can happen to Applications listed in the contained
+ TreeView.
+ """
+
+ class WindowedReactiveStar(ReactiveStar):
+
+ def __init__(self, window):
+ self._window = window
+ self._hand = Gdk.Cursor.new(Gdk.CursorType.HAND2)
+ ReactiveStar.__init__(self)
+
+ def on_enter_notify(self, widget, event):
+ self._window.get_window().set_cursor(self._hand)
+
+ def on_leave_notify(self, widget, event):
+ self._window.get_window().set_cursor(None)
+
+ __gsignals__ = {
+ # Double click on application widget
+ "application-activated" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # Show Application in the Rigo UI
+ "application-show" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # Hide Application in the Rigo UI
+ "application-hide" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # Single click on application widget
+ "application-selected" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # action requested for application
+ "application-request-action" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,
+ str),
+ ),
+ }
+
+ VOTE_NOTIFICATION_CONTEXT_ID = "VoteNotificationContext"
+ COMMENT_NOTIFICATION_CONTEXT_ID = "CommentNotificationContext"
+
+ def __init__(self, entropy_client, entropy_ws, builder):
+ GObject.Object.__init__(self)
+ self._builder = builder
+ self._entropy = entropy_client
+ self._entropy_ws = entropy_ws
+ self._app_store = None
+ self._last_app = None
+ self._nc = None
+ self._avc = None
+
+ self._window = self._builder.get_object("rigoWindow")
+ self._image = self._builder.get_object("appViewImage")
+ self._app_name_lbl = self._builder.get_object("appViewNameLabel")
+ self._app_info_lbl = self._builder.get_object("appViewInfoLabel")
+ self._app_downloaded_lbl = self._builder.get_object(
+ "appViewDownloadedLabel")
+ self._app_comments_box = self._builder.get_object("appViewCommentsVbox")
+ self._app_comments_box.set_name("comments-box")
+ self._app_comments_align = self._builder.get_object(
+ "appViewCommentsAlign")
+ self._app_my_comments_box = self._builder.get_object(
+ "appViewMyCommentsVbox")
+ self._app_my_comments_align = self._builder.get_object(
+ "appViewMyCommentsAlign")
+ self._app_my_comments_box.set_name("comments-box")
+ self._app_comment_send_button = self._builder.get_object(
+ "appViewCommentSendButton")
+ self._app_comment_text_view = self._builder.get_object(
+ "appViewCommentText")
+ self._app_comment_text_view.set_name("rigo-text-view")
+ self._app_comment_text_buffer = self._builder.get_object(
+ "appViewCommentTextBuffer")
+ self._app_comment_more_label = self._builder.get_object(
+ "appViewCommentMoreLabel")
+ self._stars_container = self._builder.get_object("appViewStarsSelVbox")
+ self._app_button_area = self._builder.get_object("appViewButtonArea")
+
+ self._stars = ApplicationViewController.WindowedReactiveStar(
+ self._window)
+ self._stars_alignment = Gtk.Alignment.new(0.0, 0.5, 1.0, 1.0)
+ self._stars_alignment.set_padding(0, 5, 0, 0)
+ self._stars_alignment.add(self._stars)
+ self._stars.set_size_as_pixel_value(24)
+
+ self._stars_container.pack_start(self._stars_alignment, False, False, 0)
+
+ self._app_images_box = self._builder.get_object(
+ "appViewImagesVbox")
+
+ def set_notification_controller(self, nc):
+ """
+ Bind NotificationController object to this class.
+ """
+ self._nc = nc
+
+ def set_applications_controller(self, avc):
+ """
+ Bind ApplicationsViewController object to this class.
+ """
+ self._avc = avc
+
+ def set_store(self, store):
+ """
+ Bind AppListStore object to this class.
+ """
+ self._app_store = store
+
+ def setup(self):
+ self.connect("application-activated", self._on_application_activated)
+ self._app_store.connect("redraw-request", self._on_redraw_request)
+ self._app_comment_send_button.connect("clicked", self._on_send_comment)
+ self._app_comment_send_button.set_sensitive(False)
+ self._app_comment_text_buffer.connect(
+ "changed", self._on_comment_buffer_changed)
+ self._stars.connect("changed", self._on_stars_clicked)
+
+ def _on_comment_buffer_changed(self, widget):
+ """
+ Our comment text is changed, decide if to activate the Send button.
+ """
+ count = self._app_comment_text_buffer.get_char_count()
+ found = count != 0
+ self._app_comment_send_button.set_sensitive(found)
+
+ def _on_application_activated(self, avc, app):
+ """
+ Event received from Gtk widgets requesting us to load package
+ information. Once we're done loading the shit, we just emit
+ 'application-show' and let others do the UI switch.
+ """
+ self._last_app = app
+ task = ParallelTask(self.__application_activate, app)
+ task.name = "ApplicationActivate"
+ task.daemon = True
+ task.start()
+
+ def _on_redraw_request(self, widget, pkg_match):
+ """
+ Redraw request received from AppListStore for given package match.
+ We are required to update rating, number of downloads, icon.
+ """
+ if self._last_app is None:
+ return
+ if pkg_match == self._last_app.get_details().pkg:
+ stats = self._app_store.get_review_stats(pkg_match)
+ icon = self._app_store.get_icon(pkg_match)
+ self._setup_application_stats(stats, icon)
+ if self._app_store is not None:
+ self._app_store.emit("redraw-request", self._app_store)
+
+ def _on_stars_clicked(self, widget, app=None):
+ """
+ Stars clicked, user wants to vote.
+ """
+ if app is None:
+ app = self._last_app
+ if app is None:
+ # wtf
+ return
+
+ def _sender(app, vote):
+ if not app.is_webservice_available():
+ GLib.idle_add(self._notify_webservice_na, app,
+ self.VOTE_NOTIFICATION_CONTEXT_ID)
+ return
+ ws_user = app.get_webservice_username()
+ if ws_user is not None:
+ GLib.idle_add(self._notify_vote_submit, app, ws_user, vote)
+ else:
+ GLib.idle_add(self._notify_login_request, app, vote,
+ self._on_stars_login_success,
+ self._on_stars_login_failed,
+ self.VOTE_NOTIFICATION_CONTEXT_ID)
+
+ vote = int(self._stars.get_rating()) # is float
+ task = ParallelTask(_sender, app, vote)
+ task.name = "AppViewSendVote"
+ task.start()
+
+ def _on_stars_login_success(self, widget, username, app):
+ """
+ Notify user that we successfully logged in!
+ """
+ box = NotificationBox(
+ _("Logged in as %s! How about your vote?") \
+ % (escape_markup(username),),
+ message_type=Gtk.MessageType.INFO,
+ context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
+
+ def _send_vote(widget):
+ self._on_stars_clicked(self._stars, app=app)
+ box.add_button(_("_Vote now"), _send_vote)
+
+ box.add_destroy_button(_("_Later"))
+ self._nc.append(box)
+
+ def _on_stars_login_failed(self, widget, app):
+ """
+ Entropy Web Services Login failed message.
+ """
+ box = NotificationBox(
+ _("Login failed. Your vote hasn't been added"),
+ message_type=Gtk.MessageType.ERROR,
+ context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
+ box.add_destroy_button(_("_Ok, thanks"))
+ self._nc.append(box)
+
+ def _notify_vote_submit(self, app, username, vote):
+ """
+ Notify User about Comment submission with current credentials.
+ """
+ box = NotificationBox(
+ _("Rate %s as %s, with %d stars?") \
+ % (app.name, escape_markup(username),
+ vote,),
+ message_type=Gtk.MessageType.INFO,
+ context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
+
+ def _vote_submit(widget):
+ self._vote_submit(app, username, vote)
+ box.add_button(_("_Ok, cool!"), _vote_submit)
+
+ def _send_vote():
+ self._on_stars_clicked(self._stars, app=app)
+ def _logout_webservice(widget):
+ self._logout_webservice(app, _send_vote)
+ box.add_button(_("_No, logout!"), _logout_webservice)
+
+ box.add_destroy_button(_("_Later"))
+ self._nc.append(box)
+
+ def _vote_submit(self, app, username, vote):
+ """
+ Do the actual vote submit.
+ """
+ task = ParallelTask(
+ self._vote_submit_thread_body,
+ app, username, vote)
+ task.name = "VoteSubmitThreadBody"
+ task.daemon = True
+ task.start()
+
+ def _vote_submit_thread_body(self, app, username, vote):
+ """
+ Called by _vote_submit(), does the actualy submit.
+ """
+ repository_id = app.get_details().channelname
+ webserv = self._entropy_ws.get(repository_id)
+ if webserv is None:
+ # impossible!
+ return
+
+ key = app.get_details().pkgkey
+
+ err_msg = None
+ try:
+ voted = webserv.add_vote(
+ key, vote)
+ except WebService.WebServiceException as err:
+ voted = False
+ err_msg = str(err)
+
+ def _submit_success():
+ nbox = NotificationBox(
+ _("Your vote has been added!"),
+ message_type=Gtk.MessageType.INFO,
+ context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
+ nbox.add_destroy_button(_("Ok, great!"))
+ self._nc.append(nbox, timeout=10)
+ self._on_redraw_request(None, app.get_details().pkg)
+
+ def _submit_fail(err_msg):
+ if err_msg is None:
+ box = NotificationBox(
+ _("You already voted this Application"),
+ message_type=Gtk.MessageType.ERROR,
+ context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
+ else:
+ box = NotificationBox(
+ _("Vote error: %s") % (err_msg,),
+ message_type=Gtk.MessageType.ERROR,
+ context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
+ box.add_destroy_button(_("Ok, thanks"))
+ self._nc.append(box)
+
+ if voted:
+ GLib.idle_add(_submit_success)
+ else:
+ GLib.idle_add(_submit_fail, err_msg)
+
+ def __application_activate(self, app):
+ """
+ Collect data from app, then call the UI setup in the main loop.
+ """
+ details = app.get_details()
+ metadata = {}
+ metadata['markup'] = app.get_extended_markup()
+ metadata['info'] = app.get_info_markup()
+ metadata['download_size'] = details.downsize
+ metadata['stats'] = app.get_review_stats()
+ metadata['homepage'] = details.website
+ metadata['date'] = details.date
+ # using app store here because we cache the icon pixbuf
+ metadata['icon'] = self._app_store.get_icon(details.pkg)
+ metadata['is_installed'] = app.is_installed()
+ metadata['is_updatable'] = app.is_updatable()
+ GLib.idle_add(self._setup_application_info, app, metadata)
+
+ def hide(self):
+ """
+ This method shall be called when the Controller widgets are
+ going to hide.
+ """
+ self._last_app = None
+ for child in self._app_my_comments_box.get_children():
+ child.destroy()
+ for child in self._app_images_box.get_children():
+ child.destroy()
+
+ self.emit("application-hide", self)
+
+ def _on_send_comment(self, widget, app=None):
+ """
+ Send comment to Web Service.
+ """
+ if app is None:
+ app = self._last_app
+ if app is None:
+ # we're hiding
+ return
+
+ text = self._app_comment_text_buffer.get_text(
+ self._app_comment_text_buffer.get_start_iter(),
+ self._app_comment_text_buffer.get_end_iter(),
+ False)
+ if not text.strip():
+ return
+ # make it utf-8
+ text = const_convert_to_unicode(text, enctype=etpConst['conf_encoding'])
+
+ def _sender(app, text):
+ if not app.is_webservice_available():
+ GLib.idle_add(self._notify_webservice_na, app,
+ self.COMMENT_NOTIFICATION_CONTEXT_ID)
+ return
+ ws_user = app.get_webservice_username()
+ if ws_user is not None:
+ GLib.idle_add(self._notify_comment_submit, app, ws_user, text)
+ else:
+ GLib.idle_add(self._notify_login_request, app, text,
+ self._on_comment_login_success,
+ self._on_comment_login_failed,
+ self.COMMENT_NOTIFICATION_CONTEXT_ID)
+
+ task = ParallelTask(_sender, app, text)
+ task.name = "AppViewSendComment"
+ task.start()
+
+ def _notify_webservice_na(self, app, context_id):
+ """
+ Notify Web Service unavailability for given Application object.
+ """
+ box = NotificationBox(
+ "%s: %s" % (
+ _("Entropy Web Services not available for repository"),
+ app.get_details().channelname),
+ message_type=Gtk.MessageType.ERROR,
+ context_id=context_id)
+ box.add_destroy_button(_("Ok, thanks"))
+ self._nc.append(box)
+
+ def _notify_comment_submit(self, app, username, text):
+ """
+ Notify User about Comment submission with current credentials.
+ """
+ box = NotificationBox(
+ _("You are about to add a comment as %s.") \
+ % (escape_markup(username),),
+ message_type=Gtk.MessageType.INFO,
+ context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
+
+ def _comment_submit(widget):
+ self._comment_submit(app, username, text)
+ box.add_button(_("_Ok, cool!"), _comment_submit)
+
+ def _send_comment():
+ self._on_send_comment(None, app=app)
+ def _logout_webservice(widget):
+ self._logout_webservice(app, _send_comment)
+ box.add_button(_("_No, logout!"), _logout_webservice)
+
+ box.add_destroy_button(_("_Later"))
+ self._nc.append(box)
+
+ def _comment_submit(self, app, username, text):
+ """
+ Actual Comment submit to Web Service.
+ Here we arrive from the MainThread.
+ """
+ task = ParallelTask(
+ self._comment_submit_thread_body,
+ app, username, text)
+ task.name = "CommentSubmitThreadBody"
+ task.daemon = True
+ task.start()
+
+ def _comment_submit_thread_body(self, app, username, text):
+ """
+ Called by _comment_submit(), does the actualy submit.
+ """
+ repository_id = app.get_details().channelname
+ webserv = self._entropy_ws.get(repository_id)
+ if webserv is None:
+ # impossible!
+ return
+
+ key = app.get_details().pkgkey
+ doc_factory = webserv.document_factory()
+ doc = doc_factory.comment(
+ username, text, "", "")
+
+ err_msg = None
+ try:
+ new_doc = webserv.add_document(key, doc)
+ except WebService.WebServiceException as err:
+ new_doc = None
+ err_msg = str(err)
+
+ def _submit_success(doc):
+ box = CommentBox(self._nc, self._avc, webserv, doc, is_last=True)
+ box.connect("destroy", self._on_comment_box_destroy)
+
+ self.__clean_my_non_comment_boxes()
+ box.render()
+ self._app_my_comments_box.pack_start(box, False, False, 2)
+ box.show()
+ self._app_my_comments_box.show()
+
+ nbox = NotificationBox(
+ _("Your comment has been submitted!"),
+ message_type=Gtk.MessageType.INFO,
+ context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
+ nbox.add_destroy_button(_("Ok, great!"))
+ self._app_comment_text_buffer.set_text("")
+ self._nc.append(nbox, timeout=10)
+
+ def _submit_fail():
+ box = NotificationBox(
+ _("Comment submit error: %s") % (err_msg,),
+ message_type=Gtk.MessageType.ERROR,
+ context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
+ box.add_destroy_button(_("Ok, thanks"))
+ self._nc.append(box)
+
+ if new_doc is not None:
+ GLib.idle_add(_submit_success, new_doc)
+ else:
+ GLib.idle_add(_submit_fail)
+
+ def _logout_webservice(self, app, reinit_callback):
+ """
+ Execute logout of current credentials from Web Service.
+ Actually, this means removing the local cookie.
+ """
+ repository_id = app.get_details().channelname
+ webserv = self._entropy_ws.get(repository_id)
+ if webserv is not None:
+ webserv.remove_credentials()
+
+ GLib.idle_add(self._avc.emit, "logged-out")
+ GLib.idle_add(reinit_callback)
+
+ def _notify_login_request(self, app, text, on_success, on_fail,
+ context_id):
+ """
+ Notify User that login is required
+ """
+ box = LoginNotificationBox(
+ self._avc, self._entropy_ws, app,
+ context_id=context_id)
+ box.connect("login-success", on_success)
+ box.connect("login-failed", on_fail)
+ self._nc.append(box)
+
+ def _on_comment_login_success(self, widget, username, app):
+ """
+ Notify user that we successfully logged in!
+ """
+ box = NotificationBox(
+ _("Logged in as %s! How about your comment?") \
+ % (escape_markup(username),),
+ message_type=Gtk.MessageType.INFO,
+ context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
+ def _send_comment(widget):
+ self._on_send_comment(widget, app=app)
+ box.add_button(_("_Send now"), _send_comment)
+ box.add_destroy_button(_("_Later"))
+ self._nc.append(box)
+
+ def _on_comment_login_failed(self, widget, app):
+ """
+ Entropy Web Services Login failed message.
+ """
+ box = NotificationBox(
+ _("Login failed. Your comment hasn't been added"),
+ message_type=Gtk.MessageType.ERROR,
+ context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
+ box.add_destroy_button(_("_Ok, thanks"))
+ self._nc.append(box)
+
+ def _on_comment_box_destroy(self, widget):
+ """
+ Called when a CommentBox is destroyed.
+ We need to figure out if there are CommentBoxes left and in case
+ show the "no comments available" message.
+ """
+ children = self._app_comments_box.get_children()
+ if not children:
+ self.__show_no_comments()
+
+ def __show_no_comments(self):
+ """
+ Create "No comments for this Application" message.
+ """
+ label = Gtk.Label()
+ label.set_markup(
+ prepare_markup(
+ _("No comments for this Application, yet!")))
+ # place in app_my, this way it will get cleared out
+ # once a new comment is inserted
+ self._app_my_comments_box.pack_start(label, False, False, 1)
+ self._app_my_comments_box.show_all()
+
+ def __clean_non_comment_boxes(self):
+ """
+ Remove children that are not CommentBox objects from
+ self._app_comments_box
+ """
+ for child in self._app_comments_box.get_children():
+ if not isinstance(child, CommentBox):
+ child.destroy()
+
+ def __clean_my_non_comment_boxes(self):
+ """
+ Remove children that are not CommentBox objects from
+ self._app_my_comments_box
+ """
+ for child in self._app_my_comments_box.get_children():
+ if not isinstance(child, CommentBox):
+ child.destroy()
+
+ def __clean_non_image_boxes(self):
+ """
+ Remove children that are not ImageBox objects from
+ self._app_images_box
+ """
+ for child in self._app_images_box.get_children():
+ if not isinstance(child, ImageBox):
+ child.destroy()
+
+ def _append_comments(self, downloader, app, comments, has_more):
+ """
+ Append given Entropy WebService Document objects to
+ the comment area.
+ """
+ self.__clean_non_comment_boxes()
+ # make sure we didn't leave stuff here as well
+ self.__clean_my_non_comment_boxes()
+
+ if not comments:
+ self.__show_no_comments()
+ return
+
+ if has_more:
+ button_box = Gtk.HButtonBox()
+ button = Gtk.Button()
+ button.set_label(_("Older comments"))
+ button.set_alignment(0.5, 0.5)
+ def _enqueue_download(widget):
+ widget.get_parent().destroy()
+ spinner = Gtk.Spinner()
+ spinner.set_size_request(24, 24)
+ spinner.set_tooltip_text(_("Loading older comments..."))
+ spinner.set_name("comment-box-spinner")
+ self._app_comments_box.pack_end(spinner, False, False, 3)
+ spinner.show()
+ spinner.start()
+ downloader.enqueue_download()
+ button.connect("clicked", _enqueue_download)
+
+ button_box.pack_start(button, False, False, 0)
+ self._app_comments_box.pack_start(button_box, False, False, 1)
+ button_box.show_all()
+
+ idx = 0
+ length = len(comments)
+ # can be None
+ webserv = self._entropy_ws.get(app.get_details().channelname)
+ for doc in comments:
+ idx += 1
+ box = CommentBox(
+ self._nc, self._avc, webserv, doc,
+ is_last=(not has_more and (idx == length)))
+ box.connect("destroy", self._on_comment_box_destroy)
+ box.render()
+ self._app_comments_box.pack_end(box, False, False, 2)
+ box.show()
+
+ def _append_comments_safe(self, downloader, app, comments, has_more):
+ """
+ Same as _append_comments() but thread-safe.
+ """
+ GLib.idle_add(self._append_comments, downloader, app,
+ comments, has_more)
+
+ def _append_images(self, downloader, app, images, has_more):
+ """
+ Append given Entropy WebService Document objects to
+ the images area.
+ """
+ self.__clean_non_image_boxes()
+
+ if not images:
+ label = Gtk.Label()
+ label.set_markup(
+ prepare_markup(
+ _("No images for this Application, yet!")))
+ self._app_images_box.pack_start(label, False, False, 1)
+ label.show()
+ return
+
+ if has_more:
+ button_box = Gtk.HButtonBox()
+ button = Gtk.Button()
+ button.set_label(_("Older images"))
+ button.set_alignment(0.5, 0.5)
+ def _enqueue_download(widget):
+ widget.get_parent().destroy()
+ spinner = Gtk.Spinner()
+ spinner.set_size_request(24, 24)
+ spinner.set_tooltip_text(_("Loading older images..."))
+ spinner.set_name("image-box-spinner")
+ self._app_images_box.pack_end(spinner, False, False, 3)
+ spinner.show()
+ spinner.start()
+ downloader.enqueue_download()
+ button.connect("clicked", _enqueue_download)
+
+ button_box.pack_start(button, False, False, 0)
+ self._app_images_box.pack_start(button_box, False, False, 1)
+ button_box.show_all()
+
+ idx = 0
+ length = len(images)
+ for doc in images:
+ idx += 1
+ box = ImageBox(doc, is_last=(not has_more and (idx == length)))
+ box.render()
+ self._app_images_box.pack_end(box, False, False, 2)
+ box.show()
+
+ def _append_images_safe(self, downloader, app, comments, has_more):
+ """
+ Same as _append_images() but thread-safe.
+ """
+ GLib.idle_add(self._append_images, downloader, app,
+ comments, has_more)
+
+ def _on_app_remove(self, widget, app):
+ """
+ Remove the given Application.
+ """
+ self.emit("application-request-action",
+ app, AppActions.REMOVE)
+
+ def _on_app_install(self, widget, app):
+ """
+ Install (or reinstall) the given Application.
+ """
+ self.emit("application-request-action",
+ app, AppActions.INSTALL)
+
+ def _setup_buttons(self, app, is_installed, is_updatable):
+ """
+ Setup Application View Buttons (Install/Remove/Update).
+ """
+ button_area = self._app_button_area
+ for child in button_area.get_children():
+ child.destroy()
+
+ if is_installed:
+ if is_updatable:
+ update_button = Gtk.Button()
+ update_button.set_label(
+ escape_markup(_("Update")))
+ def _on_app_update(widget):
+ return self._on_app_install(widget, app)
+ update_button.connect("clicked", _on_app_update)
+ button_area.pack_start(update_button, False, False, 0)
+ else:
+ reinstall_button = Gtk.Button()
+ reinstall_button.set_label(
+ escape_markup(_("Reinstall")))
+ def _on_app_reinstall(widget):
+ return self._on_app_install(widget, app)
+ reinstall_button.connect("clicked", _on_app_reinstall)
+ button_area.pack_start(reinstall_button, False, False, 0)
+
+ remove_button = Gtk.Button()
+ remove_button.set_label(
+ escape_markup(_("Remove")))
+ def _on_app_remove(widget):
+ return self._on_app_remove(widget, app)
+ remove_button.connect("clicked", _on_app_remove)
+ button_area.pack_start(remove_button, False, False, 0)
+
+ else:
+ install_button = Gtk.Button()
+ install_button.set_label(
+ escape_markup(_("Install")))
+ def _on_app_install(widget):
+ return self._on_app_install(widget, app)
+ install_button.connect("clicked", _on_app_install)
+ button_area.pack_start(install_button, False, False, 0)
+
+ button_area.show_all()
+
+ def _setup_application_stats(self, stats, icon):
+ """
+ Setup widgets related to Application statistics (and icon).
+ """
+ total_downloads = stats.downloads_total
+ if total_downloads < 0:
+ down_msg = escape_markup(_("Not available"))
+ elif not total_downloads:
+ down_msg = escape_markup(_("Never downloaded"))
+ else:
+ down_msg = "%s %s" % (
+ stats.downloads_total_markup,
+ escape_markup(_("downloads")),)
+
+ self._app_downloaded_lbl.set_markup(down_msg)
+ if icon:
+ self._image.set_from_pixbuf(icon)
+ self._stars.set_rating(stats.ratings_average)
+ self._stars_alignment.show_all()
+
+ def _setup_application_info(self, app, metadata):
+ """
+ Setup the actual UI widgets content and emit 'application-show'
+ """
+ self._app_name_lbl.set_markup(metadata['markup'])
+ self._app_info_lbl.set_markup(metadata['info'])
+
+ # install/remove/update buttons
+ self._setup_buttons(
+ app, metadata['is_installed'],
+ metadata['is_updatable'])
+
+ # only comments supported, point to the remote
+ # www service for the rest
+ self._app_comment_more_label.set_markup(
+ "%s: %s" % (
+ escape_markup(_("Want to add images, etc?")),
+ escape_markup(build_application_store_url(app, "ugc")),
+ escape_markup(_("click here!")),))
+
+ stats = metadata['stats']
+ icon = metadata['icon']
+ self._setup_application_stats(stats, icon)
+
+ # load application comments asynchronously
+ # so at the beginning, just place a spinner
+ spinner = Gtk.Spinner()
+ spinner.set_size_request(-1, 48)
+ spinner.set_tooltip_text(escape_markup(_("Loading comments...")))
+ spinner.set_name("comment-box-spinner")
+ for child in self._app_comments_box.get_children():
+ child.destroy()
+ self._app_comments_box.pack_start(spinner, False, False, 0)
+ spinner.show()
+ spinner.start()
+
+ downloader = ApplicationViewController.MetadataDownloader(
+ app, self, self._append_comments_safe,
+ app.download_comments)
+ downloader.start()
+
+ downloader = ApplicationViewController.MetadataDownloader(
+ app, self, self._append_images_safe,
+ app.download_images)
+ downloader.start()
+
+ self.emit("application-show", app)
+
+ class MetadataDownloader(GObject.Object):
+ """
+ Automated Application comments downloader.
+ """
+
+ def __init__(self, app, avc, callback, app_downloader_method):
+ self._app = app
+ self._avc = avc
+ self._offset = 0
+ self._callback = callback
+ self._task = ParallelTask(self._download)
+ self._app_downloader = app_downloader_method
+
+ def start(self):
+ """
+ Start downloading comments and send them to callback.
+ Loop over until we have more of them to download.
+ """
+ self._offset = 0
+ self._task.start()
+
+ def _download_callback(self, document_list):
+ """
+ Callback called by download_() once data
+ is arrived from web service.
+ document_list can be None!
+ """
+ has_more = 0
+ if document_list is not None:
+ has_more = document_list.has_more()
+ # stash more data?
+ if has_more and (document_list is not None):
+ self._offset += len(document_list)
+ # download() will be called externally
+
+ if const_debug_enabled():
+ const_debug_write(
+ __name__,
+ "MetadataDownloader._download_callback: %s, more: %s" % (
+ document_list, has_more))
+ if document_list is not None:
+ const_debug_write(
+ __name__,
+ "MetadataDownloader._download_callback: "
+ "total: %s, offset: %s" % (
+ document_list.total(), document_list.offset()))
+
+ self._callback(self, self._app, document_list, has_more)
+
+ def reset_offset(self):
+ """
+ Reset Metadata download offset to 0.
+ """
+ self._offset = 0
+
+ def get_offset(self):
+ """
+ Get current Metadata download offset.
+ """
+ return self._offset
+
+ def enqueue_download(self):
+ """
+ Enqueue a new download, starting from current offset
+ """
+ self._task = ParallelTask(self._download)
+ self._task.start()
+
+ def _download(self):
+ """
+ Thread body of the initial Metadata downloader.
+ """
+ self._app_downloader(self._download_callback,
+ offset=self._offset)
diff --git a/rigo/rigo/ui/gtk3/controllers/applications.py b/rigo/rigo/ui/gtk3/controllers/applications.py
new file mode 100644
index 000000000..c955114fb
--- /dev/null
+++ b/rigo/rigo/ui/gtk3/controllers/applications.py
@@ -0,0 +1,297 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2012 Fabio Erculiani
+
+Authors:
+ Fabio Erculiani
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+details.
+
+You should have received a copy of the GNU General Public License along with
+this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+
+import copy
+import dbus
+
+from gi.repository import Gtk, GLib, GObject
+
+from rigo.models.application import Application, ApplicationMetadata
+from rigo.utils import escape_markup, prepare_markup
+
+from entropy.const import etpConst, const_debug_write, \
+ const_debug_enabled, const_convert_to_unicode
+from entropy.misc import ParallelTask
+from entropy.i18n import _
+
+
+class ApplicationsViewController(GObject.Object):
+
+ __gsignals__ = {
+ # View has been cleared
+ "view-cleared" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ tuple(),
+ ),
+ # View has been filled
+ "view-filled" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ tuple(),
+ ),
+ # View has been filled
+ "view-want-change" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # User logged in to Entropy Web Services
+ "logged-in" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ (GObject.TYPE_PYOBJECT,),
+ ),
+ # User logged out from Entropy Web Services
+ "logged-out" : (GObject.SignalFlags.RUN_LAST,
+ None,
+ tuple(),
+ ),
+ }
+
+ def __init__(self, activity_rwsem, entropy_client, entropy_ws,
+ rigo_service, icons, nf_box,
+ search_entry, store, view):
+ GObject.Object.__init__(self)
+ self._activity_rwsem = activity_rwsem
+ self._entropy = entropy_client
+ self._service = rigo_service
+ self._icons = icons
+ self._entropy_ws = entropy_ws
+ self._search_entry = search_entry
+ self._store = store
+ self._view = view
+ self._nf_box = nf_box
+ self._not_found_search_box = None
+ self._not_found_label = None
+
+ def _search_icon_release(self, search_entry, icon_pos, _other):
+ """
+ Event associated to the Search bar icon click.
+ Here we catch secondary icon click to reset the search entry text.
+ """
+ if search_entry is not self._search_entry:
+ return
+ if icon_pos == Gtk.EntryIconPosition.SECONDARY:
+ search_entry.set_text("")
+ self.clear()
+ search_entry.emit("changed")
+ elif self._store.get_iter_first():
+ # primary icon click will force UI to switch to Browser mode
+ self.emit("view-filled")
+
+ def _search_changed(self, search_entry):
+ GLib.timeout_add(700, self._search, search_entry.get_text())
+
+ def _search(self, old_text):
+ cur_text = self._search_entry.get_text()
+ if cur_text == old_text and cur_text:
+ search_text = copy.copy(old_text)
+ search_text = const_convert_to_unicode(
+ search_text, enctype=etpConst['conf_encoding'])
+ th = ParallelTask(self.__search_thread, search_text)
+ th.name = "SearchThread"
+ th.start()
+
+ def __search_thread(self, text):
+ def _prepare_for_search(txt):
+ return txt.replace(" ", "-").lower()
+
+ ## special keywords hook
+ if text == "rigo:update":
+ self._update_repositories_safe()
+ return
+ if text == "rigo:vte":
+ GLib.idle_add(self.emit, "view-want-change", Rigo.WORK_VIEW_STATE)
+ return
+ if text == "rigo:output":
+ GLib.idle_add(self.emit, "view-want-change", Rigo.WORK_VIEW_STATE)
+ GLib.idle_add(self._service.output_test)
+ return
+
+ # Do not execute search if repositories are
+ # being hold by other write
+ self._activity_rwsem.reader_acquire()
+ try:
+
+ matches = []
+
+ # exact match
+ pkg_matches, rc = self._entropy.atom_match(
+ text, multi_match = True,
+ multi_repo = True, mask_filter = False)
+ matches.extend(pkg_matches)
+
+ # atom searching (name and desc)
+ search_matches = self._entropy.atom_search(
+ text,
+ repositories = self._entropy.repositories(),
+ description = True)
+
+ matches.extend([x for x in search_matches if x not in matches])
+
+ if not search_matches:
+ search_matches = self._entropy.atom_search(
+ _prepare_for_search(text),
+ repositories = self._entropy.repositories())
+ matches.extend([x for x in search_matches if x not in matches])
+
+ # we have to decide if to show the treeview in
+ # the UI thread, to avoid races (and also because we
+ # have to...)
+ self.set_many_safe(matches, _from_search=text)
+
+ finally:
+ self._activity_rwsem.reader_release()
+
+ def _setup_search_view(self, items_count, text):
+ """
+ Setup UI in order to show a "not found" message if required.
+ """
+ nf_box = self._not_found_box
+ if items_count:
+ nf_box.set_property("expand", False)
+ nf_box.hide()
+ self._view.get_parent().show()
+ else:
+ self._view.get_parent().hide()
+ self._setup_not_found_box(text)
+ nf_box.set_property("expand", True)
+ nf_box.show()
+
+ def _setup_not_found_box(self, search_text):
+ """
+ Setup "not found" message label and layout
+ """
+ nf_box = self._not_found_box
+ # now self._not_found_label is available
+ meant_packages = self._entropy.get_meant_packages(
+ search_text)
+ text = escape_markup(search_text)
+
+ msg = "%s %s" % (
+ escape_markup(_("Nothing found for")),
+ text,)
+ if meant_packages:
+ first_entry = meant_packages[0]
+ app = Application(
+ self._entropy, self._entropy_ws,
+ first_entry)
+ name = app.name
+
+ msg += ", %s" % (
+ prepare_markup(_("did you mean %s?")) % (
+ escape_markup(name),
+ escape_markup(name),),)
+
+ self._not_found_label.set_markup(msg)
+
+ def _on_not_found_label_activate_link(self, label, text):
+ """
+ Handling the click event on of the
+ "not found" search label. Just write the coming text
+ to the Gtk.SearchEntry object.
+ """
+ if text:
+ self._search_entry.set_text(text)
+ self._search(text)
+
+ @property
+ def _not_found_box(self):
+ """
+ Return a Gtk.VBox containing the view that should
+ be shown when no apps have been found (due to a search).
+ """
+ if self._not_found_search_box is not None:
+ return self._not_found_search_box
+ # here we always have to access from the same thread
+ # otherwise Gtk will go boom anyway
+ box_align = Gtk.Alignment()
+ box_align.set_padding(10, 10, 0, 0)
+ box = Gtk.VBox()
+ box_align.add(box)
+ label = Gtk.Label(_("Not found"))
+ label.connect("activate-link", self._on_not_found_label_activate_link)
+ box.pack_start(label, True, True, 0)
+ box_align.show()
+
+ self._nf_box.pack_start(box_align, False, False, 0)
+ self._nf_box.show_all()
+ self._not_found_label = label
+ self._not_found_search_box = box_align
+ return box_align
+
+ def _update_repositories(self):
+ """
+ Spawn Repository Update on RigoDaemon
+ """
+ self._service.update_repositories([], True)
+
+ def _update_repositories_safe(self):
+ """
+ Same as _update_repositories() but thread safe.
+ """
+ GLib.idle_add(self._update_repositories)
+
+ def setup(self):
+ self._view.set_model(self._store)
+ self._search_entry.connect(
+ "changed", self._search_changed)
+ self._search_entry.connect("icon-release",
+ self._search_icon_release)
+ self._view.show()
+
+ def clear(self):
+ self._store.clear()
+ ApplicationMetadata.discard()
+ if const_debug_enabled():
+ const_debug_write(__name__, "AVC: emitting view-cleared")
+ self.emit("view-cleared")
+
+ def append(self, opaque):
+ self._store.append([opaque])
+ if const_debug_enabled():
+ const_debug_write(__name__, "AVC: emitting view-filled")
+ self.emit("view-filled")
+
+ def append_many(self, opaque_list):
+ for opaque in opaque_list:
+ self._store.append([opaque])
+ if const_debug_enabled():
+ const_debug_write(__name__, "AVC: emitting view-filled")
+ self.emit("view-filled")
+
+ def set_many(self, opaque_list, _from_search=None):
+ self._store.clear()
+ ApplicationMetadata.discard()
+ self.append_many(opaque_list)
+ if _from_search:
+ self._setup_search_view(
+ len(opaque_list), _from_search)
+
+ def clear_safe(self):
+ GLib.idle_add(self.clear)
+
+ def append_safe(self, opaque):
+ GLib.idle_add(self.append, opaque)
+
+ def append_many_safe(self, opaque_list):
+ GLib.idle_add(self.append_many, opaque_list)
+
+ def set_many_safe(self, opaque_list, _from_search=None):
+ GLib.idle_add(self.set_many, opaque_list,
+ _from_search)
diff --git a/rigo/rigo_app.py b/rigo/rigo_app.py
index a43dc1a24..5b8def5f1 100644
--- a/rigo/rigo_app.py
+++ b/rigo/rigo_app.py
@@ -36,9 +36,8 @@ sys.path.insert(4, "/usr/lib/entropy/client")
sys.path.insert(5, "/usr/lib/entropy/rigo")
sys.path.insert(6, "/usr/lib/rigo")
-import gi
-gi.require_version("Gtk", "3.0")
-from gi.repository import Gtk, Gdk, Gio, GLib, GObject, Vte, Pango
+from gi.repository import Gtk, Gdk, Gio, GLib, GObject, Vte, Pango, \
+ Polkit
from rigo.paths import DATA_DIR
from rigo.enums import Icons, AppActions
@@ -48,6 +47,8 @@ from rigo.ui.gtk3.widgets.apptreeview import AppTreeView
from rigo.ui.gtk3.widgets.notifications import NotificationBox, \
RepositoriesUpdateNotificationBox, UpdatesNotificationBox, \
LoginNotificationBox, ConnectivityNotificationBox
+from rigo.ui.gtk3.controllers.applications import ApplicationsViewController
+from rigo.ui.gtk3.controllers.application import ApplicationViewController
from rigo.ui.gtk3.widgets.welcome import WelcomeBox
from rigo.ui.gtk3.widgets.stars import ReactiveStar
from rigo.ui.gtk3.widgets.comments import CommentBox
@@ -880,270 +881,6 @@ class WorkViewController(GObject.Object):
self._terminal.select_all()
-class ApplicationsViewController(GObject.Object):
-
- __gsignals__ = {
- # View has been cleared
- "view-cleared" : (GObject.SignalFlags.RUN_LAST,
- None,
- tuple(),
- ),
- # View has been filled
- "view-filled" : (GObject.SignalFlags.RUN_LAST,
- None,
- tuple(),
- ),
- # View has been filled
- "view-want-change" : (GObject.SignalFlags.RUN_LAST,
- None,
- (GObject.TYPE_PYOBJECT,),
- ),
- # User logged in to Entropy Web Services
- "logged-in" : (GObject.SignalFlags.RUN_LAST,
- None,
- (GObject.TYPE_PYOBJECT,),
- ),
- # User logged out from Entropy Web Services
- "logged-out" : (GObject.SignalFlags.RUN_LAST,
- None,
- tuple(),
- ),
- }
-
- def __init__(self, activity_rwsem, entropy_client, entropy_ws,
- rigo_service, icons, nf_box,
- search_entry, store, view):
- GObject.Object.__init__(self)
- self._activity_rwsem = activity_rwsem
- self._entropy = entropy_client
- self._service = rigo_service
- self._icons = icons
- self._entropy_ws = entropy_ws
- self._search_entry = search_entry
- self._store = store
- self._view = view
- self._nf_box = nf_box
- self._not_found_search_box = None
- self._not_found_label = None
-
- def _search_icon_release(self, search_entry, icon_pos, _other):
- """
- Event associated to the Search bar icon click.
- Here we catch secondary icon click to reset the search entry text.
- """
- if search_entry is not self._search_entry:
- return
- if icon_pos == Gtk.EntryIconPosition.SECONDARY:
- search_entry.set_text("")
- self.clear()
- search_entry.emit("changed")
- elif self._store.get_iter_first():
- # primary icon click will force UI to switch to Browser mode
- self.emit("view-filled")
-
- def _search_changed(self, search_entry):
- GLib.timeout_add(700, self._search, search_entry.get_text())
-
- def _search(self, old_text):
- cur_text = self._search_entry.get_text()
- if cur_text == old_text and cur_text:
- search_text = copy.copy(old_text)
- search_text = const_convert_to_unicode(
- search_text, enctype=etpConst['conf_encoding'])
- th = ParallelTask(self.__search_thread, search_text)
- th.name = "SearchThread"
- th.start()
-
- def __search_thread(self, text):
- def _prepare_for_search(txt):
- return txt.replace(" ", "-").lower()
-
- ## special keywords hook
- if text == "rigo:update":
- self._update_repositories_safe()
- return
- if text == "rigo:vte":
- GLib.idle_add(self.emit, "view-want-change", Rigo.WORK_VIEW_STATE)
- return
- if text == "rigo:output":
- GLib.idle_add(self.emit, "view-want-change", Rigo.WORK_VIEW_STATE)
- GLib.idle_add(self._service.output_test)
- return
-
- # Do not execute search if repositories are
- # being hold by other write
- self._activity_rwsem.reader_acquire()
- try:
-
- matches = []
-
- # exact match
- pkg_matches, rc = self._entropy.atom_match(
- text, multi_match = True,
- multi_repo = True, mask_filter = False)
- matches.extend(pkg_matches)
-
- # atom searching (name and desc)
- search_matches = self._entropy.atom_search(
- text,
- repositories = self._entropy.repositories(),
- description = True)
-
- matches.extend([x for x in search_matches if x not in matches])
-
- if not search_matches:
- search_matches = self._entropy.atom_search(
- _prepare_for_search(text),
- repositories = self._entropy.repositories())
- matches.extend([x for x in search_matches if x not in matches])
-
- # we have to decide if to show the treeview in
- # the UI thread, to avoid races (and also because we
- # have to...)
- self.set_many_safe(matches, _from_search=text)
-
- finally:
- self._activity_rwsem.reader_release()
-
- def _setup_search_view(self, items_count, text):
- """
- Setup UI in order to show a "not found" message if required.
- """
- nf_box = self._not_found_box
- if items_count:
- nf_box.set_property("expand", False)
- nf_box.hide()
- self._view.get_parent().show()
- else:
- self._view.get_parent().hide()
- self._setup_not_found_box(text)
- nf_box.set_property("expand", True)
- nf_box.show()
-
- def _setup_not_found_box(self, search_text):
- """
- Setup "not found" message label and layout
- """
- nf_box = self._not_found_box
- # now self._not_found_label is available
- meant_packages = self._entropy.get_meant_packages(
- search_text)
- text = escape_markup(search_text)
-
- msg = "%s %s" % (
- escape_markup(_("Nothing found for")),
- text,)
- if meant_packages:
- first_entry = meant_packages[0]
- app = Application(
- self._entropy, self._entropy_ws,
- first_entry)
- name = app.name
-
- msg += ", %s" % (
- prepare_markup(_("did you mean %s?")) % (
- escape_markup(name),
- escape_markup(name),),)
-
- self._not_found_label.set_markup(msg)
-
- def _on_not_found_label_activate_link(self, label, text):
- """
- Handling the click event on of the
- "not found" search label. Just write the coming text
- to the Gtk.SearchEntry object.
- """
- if text:
- self._search_entry.set_text(text)
- self._search(text)
-
- @property
- def _not_found_box(self):
- """
- Return a Gtk.VBox containing the view that should
- be shown when no apps have been found (due to a search).
- """
- if self._not_found_search_box is not None:
- return self._not_found_search_box
- # here we always have to access from the same thread
- # otherwise Gtk will go boom anyway
- box_align = Gtk.Alignment()
- box_align.set_padding(10, 10, 0, 0)
- box = Gtk.VBox()
- box_align.add(box)
- label = Gtk.Label(_("Not found"))
- label.connect("activate-link", self._on_not_found_label_activate_link)
- box.pack_start(label, True, True, 0)
- box_align.show()
-
- self._nf_box.pack_start(box_align, False, False, 0)
- self._nf_box.show_all()
- self._not_found_label = label
- self._not_found_search_box = box_align
- return box_align
-
- def _update_repositories(self):
- """
- Spawn Repository Update on RigoDaemon
- """
- self._service.update_repositories([], True)
-
- def _update_repositories_safe(self):
- """
- Same as _update_repositories() but thread safe.
- """
- GLib.idle_add(self._update_repositories)
-
- def setup(self):
- self._view.set_model(self._store)
- self._search_entry.connect(
- "changed", self._search_changed)
- self._search_entry.connect("icon-release",
- self._search_icon_release)
- self._view.show()
-
- def clear(self):
- self._store.clear()
- ApplicationMetadata.discard()
- if const_debug_enabled():
- const_debug_write(__name__, "AVC: emitting view-cleared")
- self.emit("view-cleared")
-
- def append(self, opaque):
- self._store.append([opaque])
- if const_debug_enabled():
- const_debug_write(__name__, "AVC: emitting view-filled")
- self.emit("view-filled")
-
- def append_many(self, opaque_list):
- for opaque in opaque_list:
- self._store.append([opaque])
- if const_debug_enabled():
- const_debug_write(__name__, "AVC: emitting view-filled")
- self.emit("view-filled")
-
- def set_many(self, opaque_list, _from_search=None):
- self._store.clear()
- ApplicationMetadata.discard()
- self.append_many(opaque_list)
- if _from_search:
- self._setup_search_view(
- len(opaque_list), _from_search)
-
- def clear_safe(self):
- GLib.idle_add(self.clear)
-
- def append_safe(self, opaque):
- GLib.idle_add(self.append, opaque)
-
- def append_many_safe(self, opaque_list):
- GLib.idle_add(self.append_many, opaque_list)
-
- def set_many_safe(self, opaque_list, _from_search=None):
- GLib.idle_add(self.set_many, opaque_list,
- _from_search)
-
-
class NotificationViewController(GObject.Object):
"""
@@ -1341,889 +1078,6 @@ class NotificationViewController(GObject.Object):
GLib.idle_add(self.clear)
-class ApplicationViewController(GObject.Object):
- """
- Applications View Container, exposing all the events
- that can happen to Applications listed in the contained
- TreeView.
- """
-
- class WindowedReactiveStar(ReactiveStar):
-
- def __init__(self, window):
- self._window = window
- self._hand = Gdk.Cursor.new(Gdk.CursorType.HAND2)
- ReactiveStar.__init__(self)
-
- def on_enter_notify(self, widget, event):
- self._window.get_window().set_cursor(self._hand)
-
- def on_leave_notify(self, widget, event):
- self._window.get_window().set_cursor(None)
-
- __gsignals__ = {
- # Double click on application widget
- "application-activated" : (GObject.SignalFlags.RUN_LAST,
- None,
- (GObject.TYPE_PYOBJECT,),
- ),
- # Show Application in the Rigo UI
- "application-show" : (GObject.SignalFlags.RUN_LAST,
- None,
- (GObject.TYPE_PYOBJECT,),
- ),
- # Hide Application in the Rigo UI
- "application-hide" : (GObject.SignalFlags.RUN_LAST,
- None,
- (GObject.TYPE_PYOBJECT,),
- ),
- # Single click on application widget
- "application-selected" : (GObject.SignalFlags.RUN_LAST,
- None,
- (GObject.TYPE_PYOBJECT,),
- ),
- # action requested for application
- "application-request-action" : (GObject.SignalFlags.RUN_LAST,
- None,
- (GObject.TYPE_PYOBJECT,
- str),
- ),
- }
-
- VOTE_NOTIFICATION_CONTEXT_ID = "VoteNotificationContext"
- COMMENT_NOTIFICATION_CONTEXT_ID = "CommentNotificationContext"
-
- def __init__(self, entropy_client, entropy_ws, builder):
- GObject.Object.__init__(self)
- self._builder = builder
- self._entropy = entropy_client
- self._entropy_ws = entropy_ws
- self._app_store = None
- self._last_app = None
- self._nc = None
- self._avc = None
-
- self._window = self._builder.get_object("rigoWindow")
- self._image = self._builder.get_object("appViewImage")
- self._app_name_lbl = self._builder.get_object("appViewNameLabel")
- self._app_info_lbl = self._builder.get_object("appViewInfoLabel")
- self._app_downloaded_lbl = self._builder.get_object(
- "appViewDownloadedLabel")
- self._app_comments_box = self._builder.get_object("appViewCommentsVbox")
- self._app_comments_box.set_name("comments-box")
- self._app_comments_align = self._builder.get_object(
- "appViewCommentsAlign")
- self._app_my_comments_box = self._builder.get_object(
- "appViewMyCommentsVbox")
- self._app_my_comments_align = self._builder.get_object(
- "appViewMyCommentsAlign")
- self._app_my_comments_box.set_name("comments-box")
- self._app_comment_send_button = self._builder.get_object(
- "appViewCommentSendButton")
- self._app_comment_text_view = self._builder.get_object(
- "appViewCommentText")
- self._app_comment_text_view.set_name("rigo-text-view")
- self._app_comment_text_buffer = self._builder.get_object(
- "appViewCommentTextBuffer")
- self._app_comment_more_label = self._builder.get_object(
- "appViewCommentMoreLabel")
- self._stars_container = self._builder.get_object("appViewStarsSelVbox")
- self._app_button_area = self._builder.get_object("appViewButtonArea")
-
- self._stars = ApplicationViewController.WindowedReactiveStar(
- self._window)
- self._stars_alignment = Gtk.Alignment.new(0.0, 0.5, 1.0, 1.0)
- self._stars_alignment.set_padding(0, 5, 0, 0)
- self._stars_alignment.add(self._stars)
- self._stars.set_size_as_pixel_value(24)
-
- self._stars_container.pack_start(self._stars_alignment, False, False, 0)
-
- self._app_images_box = self._builder.get_object(
- "appViewImagesVbox")
-
- def set_notification_controller(self, nc):
- """
- Bind NotificationController object to this class.
- """
- self._nc = nc
-
- def set_applications_controller(self, avc):
- """
- Bind ApplicationsViewController object to this class.
- """
- self._avc = avc
-
- def set_store(self, store):
- """
- Bind AppListStore object to this class.
- """
- self._app_store = store
-
- def setup(self):
- self.connect("application-activated", self._on_application_activated)
- self._app_store.connect("redraw-request", self._on_redraw_request)
- self._app_comment_send_button.connect("clicked", self._on_send_comment)
- self._app_comment_send_button.set_sensitive(False)
- self._app_comment_text_buffer.connect(
- "changed", self._on_comment_buffer_changed)
- self._stars.connect("changed", self._on_stars_clicked)
-
- def _on_comment_buffer_changed(self, widget):
- """
- Our comment text is changed, decide if to activate the Send button.
- """
- count = self._app_comment_text_buffer.get_char_count()
- found = count != 0
- self._app_comment_send_button.set_sensitive(found)
-
- def _on_application_activated(self, avc, app):
- """
- Event received from Gtk widgets requesting us to load package
- information. Once we're done loading the shit, we just emit
- 'application-show' and let others do the UI switch.
- """
- self._last_app = app
- task = ParallelTask(self.__application_activate, app)
- task.name = "ApplicationActivate"
- task.daemon = True
- task.start()
-
- def _on_redraw_request(self, widget, pkg_match):
- """
- Redraw request received from AppListStore for given package match.
- We are required to update rating, number of downloads, icon.
- """
- if self._last_app is None:
- return
- if pkg_match == self._last_app.get_details().pkg:
- stats = self._app_store.get_review_stats(pkg_match)
- icon = self._app_store.get_icon(pkg_match)
- self._setup_application_stats(stats, icon)
- if self._app_store is not None:
- self._app_store.emit("redraw-request", self._app_store)
-
- def _on_stars_clicked(self, widget, app=None):
- """
- Stars clicked, user wants to vote.
- """
- if app is None:
- app = self._last_app
- if app is None:
- # wtf
- return
-
- def _sender(app, vote):
- if not app.is_webservice_available():
- GLib.idle_add(self._notify_webservice_na, app,
- self.VOTE_NOTIFICATION_CONTEXT_ID)
- return
- ws_user = app.get_webservice_username()
- if ws_user is not None:
- GLib.idle_add(self._notify_vote_submit, app, ws_user, vote)
- else:
- GLib.idle_add(self._notify_login_request, app, vote,
- self._on_stars_login_success,
- self._on_stars_login_failed,
- self.VOTE_NOTIFICATION_CONTEXT_ID)
-
- vote = int(self._stars.get_rating()) # is float
- task = ParallelTask(_sender, app, vote)
- task.name = "AppViewSendVote"
- task.start()
-
- def _on_stars_login_success(self, widget, username, app):
- """
- Notify user that we successfully logged in!
- """
- box = NotificationBox(
- _("Logged in as %s! How about your vote?") \
- % (escape_markup(username),),
- message_type=Gtk.MessageType.INFO,
- context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
-
- def _send_vote(widget):
- self._on_stars_clicked(self._stars, app=app)
- box.add_button(_("_Vote now"), _send_vote)
-
- box.add_destroy_button(_("_Later"))
- self._nc.append(box)
-
- def _on_stars_login_failed(self, widget, app):
- """
- Entropy Web Services Login failed message.
- """
- box = NotificationBox(
- _("Login failed. Your vote hasn't been added"),
- message_type=Gtk.MessageType.ERROR,
- context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
- box.add_destroy_button(_("_Ok, thanks"))
- self._nc.append(box)
-
- def _notify_vote_submit(self, app, username, vote):
- """
- Notify User about Comment submission with current credentials.
- """
- box = NotificationBox(
- _("Rate %s as %s, with %d stars?") \
- % (app.name, escape_markup(username),
- vote,),
- message_type=Gtk.MessageType.INFO,
- context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
-
- def _vote_submit(widget):
- self._vote_submit(app, username, vote)
- box.add_button(_("_Ok, cool!"), _vote_submit)
-
- def _send_vote():
- self._on_stars_clicked(self._stars, app=app)
- def _logout_webservice(widget):
- self._logout_webservice(app, _send_vote)
- box.add_button(_("_No, logout!"), _logout_webservice)
-
- box.add_destroy_button(_("_Later"))
- self._nc.append(box)
-
- def _vote_submit(self, app, username, vote):
- """
- Do the actual vote submit.
- """
- task = ParallelTask(
- self._vote_submit_thread_body,
- app, username, vote)
- task.name = "VoteSubmitThreadBody"
- task.daemon = True
- task.start()
-
- def _vote_submit_thread_body(self, app, username, vote):
- """
- Called by _vote_submit(), does the actualy submit.
- """
- repository_id = app.get_details().channelname
- webserv = self._entropy_ws.get(repository_id)
- if webserv is None:
- # impossible!
- return
-
- key = app.get_details().pkgkey
-
- err_msg = None
- try:
- voted = webserv.add_vote(
- key, vote)
- except WebService.WebServiceException as err:
- voted = False
- err_msg = str(err)
-
- def _submit_success():
- nbox = NotificationBox(
- _("Your vote has been added!"),
- message_type=Gtk.MessageType.INFO,
- context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
- nbox.add_destroy_button(_("Ok, great!"))
- self._nc.append(nbox, timeout=10)
- self._on_redraw_request(None, app.get_details().pkg)
-
- def _submit_fail(err_msg):
- if err_msg is None:
- box = NotificationBox(
- _("You already voted this Application"),
- message_type=Gtk.MessageType.ERROR,
- context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
- else:
- box = NotificationBox(
- _("Vote error: %s") % (err_msg,),
- message_type=Gtk.MessageType.ERROR,
- context_id=self.VOTE_NOTIFICATION_CONTEXT_ID)
- box.add_destroy_button(_("Ok, thanks"))
- self._nc.append(box)
-
- if voted:
- GLib.idle_add(_submit_success)
- else:
- GLib.idle_add(_submit_fail, err_msg)
-
- def __application_activate(self, app):
- """
- Collect data from app, then call the UI setup in the main loop.
- """
- details = app.get_details()
- metadata = {}
- metadata['markup'] = app.get_extended_markup()
- metadata['info'] = app.get_info_markup()
- metadata['download_size'] = details.downsize
- metadata['stats'] = app.get_review_stats()
- metadata['homepage'] = details.website
- metadata['date'] = details.date
- # using app store here because we cache the icon pixbuf
- metadata['icon'] = self._app_store.get_icon(details.pkg)
- metadata['is_installed'] = app.is_installed()
- metadata['is_updatable'] = app.is_updatable()
- GLib.idle_add(self._setup_application_info, app, metadata)
-
- def hide(self):
- """
- This method shall be called when the Controller widgets are
- going to hide.
- """
- self._last_app = None
- for child in self._app_my_comments_box.get_children():
- child.destroy()
- for child in self._app_images_box.get_children():
- child.destroy()
-
- self.emit("application-hide", self)
-
- def _on_send_comment(self, widget, app=None):
- """
- Send comment to Web Service.
- """
- if app is None:
- app = self._last_app
- if app is None:
- # we're hiding
- return
-
- text = self._app_comment_text_buffer.get_text(
- self._app_comment_text_buffer.get_start_iter(),
- self._app_comment_text_buffer.get_end_iter(),
- False)
- if not text.strip():
- return
- # make it utf-8
- text = const_convert_to_unicode(text, enctype=etpConst['conf_encoding'])
-
- def _sender(app, text):
- if not app.is_webservice_available():
- GLib.idle_add(self._notify_webservice_na, app,
- self.COMMENT_NOTIFICATION_CONTEXT_ID)
- return
- ws_user = app.get_webservice_username()
- if ws_user is not None:
- GLib.idle_add(self._notify_comment_submit, app, ws_user, text)
- else:
- GLib.idle_add(self._notify_login_request, app, text,
- self._on_comment_login_success,
- self._on_comment_login_failed,
- self.COMMENT_NOTIFICATION_CONTEXT_ID)
-
- task = ParallelTask(_sender, app, text)
- task.name = "AppViewSendComment"
- task.start()
-
- def _notify_webservice_na(self, app, context_id):
- """
- Notify Web Service unavailability for given Application object.
- """
- box = NotificationBox(
- "%s: %s" % (
- _("Entropy Web Services not available for repository"),
- app.get_details().channelname),
- message_type=Gtk.MessageType.ERROR,
- context_id=context_id)
- box.add_destroy_button(_("Ok, thanks"))
- self._nc.append(box)
-
- def _notify_comment_submit(self, app, username, text):
- """
- Notify User about Comment submission with current credentials.
- """
- box = NotificationBox(
- _("You are about to add a comment as %s.") \
- % (escape_markup(username),),
- message_type=Gtk.MessageType.INFO,
- context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
-
- def _comment_submit(widget):
- self._comment_submit(app, username, text)
- box.add_button(_("_Ok, cool!"), _comment_submit)
-
- def _send_comment():
- self._on_send_comment(None, app=app)
- def _logout_webservice(widget):
- self._logout_webservice(app, _send_comment)
- box.add_button(_("_No, logout!"), _logout_webservice)
-
- box.add_destroy_button(_("_Later"))
- self._nc.append(box)
-
- def _comment_submit(self, app, username, text):
- """
- Actual Comment submit to Web Service.
- Here we arrive from the MainThread.
- """
- task = ParallelTask(
- self._comment_submit_thread_body,
- app, username, text)
- task.name = "CommentSubmitThreadBody"
- task.daemon = True
- task.start()
-
- def _comment_submit_thread_body(self, app, username, text):
- """
- Called by _comment_submit(), does the actualy submit.
- """
- repository_id = app.get_details().channelname
- webserv = self._entropy_ws.get(repository_id)
- if webserv is None:
- # impossible!
- return
-
- key = app.get_details().pkgkey
- doc_factory = webserv.document_factory()
- doc = doc_factory.comment(
- username, text, "", "")
-
- err_msg = None
- try:
- new_doc = webserv.add_document(key, doc)
- except WebService.WebServiceException as err:
- new_doc = None
- err_msg = str(err)
-
- def _submit_success(doc):
- box = CommentBox(self._nc, self._avc, webserv, doc, is_last=True)
- box.connect("destroy", self._on_comment_box_destroy)
-
- self.__clean_my_non_comment_boxes()
- box.render()
- self._app_my_comments_box.pack_start(box, False, False, 2)
- box.show()
- self._app_my_comments_box.show()
-
- nbox = NotificationBox(
- _("Your comment has been submitted!"),
- message_type=Gtk.MessageType.INFO,
- context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
- nbox.add_destroy_button(_("Ok, great!"))
- self._app_comment_text_buffer.set_text("")
- self._nc.append(nbox, timeout=10)
-
- def _submit_fail():
- box = NotificationBox(
- _("Comment submit error: %s") % (err_msg,),
- message_type=Gtk.MessageType.ERROR,
- context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
- box.add_destroy_button(_("Ok, thanks"))
- self._nc.append(box)
-
- if new_doc is not None:
- GLib.idle_add(_submit_success, new_doc)
- else:
- GLib.idle_add(_submit_fail)
-
- def _logout_webservice(self, app, reinit_callback):
- """
- Execute logout of current credentials from Web Service.
- Actually, this means removing the local cookie.
- """
- repository_id = app.get_details().channelname
- webserv = self._entropy_ws.get(repository_id)
- if webserv is not None:
- webserv.remove_credentials()
-
- GLib.idle_add(self._avc.emit, "logged-out")
- GLib.idle_add(reinit_callback)
-
- def _notify_login_request(self, app, text, on_success, on_fail,
- context_id):
- """
- Notify User that login is required
- """
- box = LoginNotificationBox(
- self._avc, self._entropy_ws, app,
- context_id=context_id)
- box.connect("login-success", on_success)
- box.connect("login-failed", on_fail)
- self._nc.append(box)
-
- def _on_comment_login_success(self, widget, username, app):
- """
- Notify user that we successfully logged in!
- """
- box = NotificationBox(
- _("Logged in as %s! How about your comment?") \
- % (escape_markup(username),),
- message_type=Gtk.MessageType.INFO,
- context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
- def _send_comment(widget):
- self._on_send_comment(widget, app=app)
- box.add_button(_("_Send now"), _send_comment)
- box.add_destroy_button(_("_Later"))
- self._nc.append(box)
-
- def _on_comment_login_failed(self, widget, app):
- """
- Entropy Web Services Login failed message.
- """
- box = NotificationBox(
- _("Login failed. Your comment hasn't been added"),
- message_type=Gtk.MessageType.ERROR,
- context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
- box.add_destroy_button(_("_Ok, thanks"))
- self._nc.append(box)
-
- def _on_comment_box_destroy(self, widget):
- """
- Called when a CommentBox is destroyed.
- We need to figure out if there are CommentBoxes left and in case
- show the "no comments available" message.
- """
- children = self._app_comments_box.get_children()
- if not children:
- self.__show_no_comments()
-
- def __show_no_comments(self):
- """
- Create "No comments for this Application" message.
- """
- label = Gtk.Label()
- label.set_markup(
- prepare_markup(
- _("No comments for this Application, yet!")))
- # place in app_my, this way it will get cleared out
- # once a new comment is inserted
- self._app_my_comments_box.pack_start(label, False, False, 1)
- self._app_my_comments_box.show_all()
-
- def __clean_non_comment_boxes(self):
- """
- Remove children that are not CommentBox objects from
- self._app_comments_box
- """
- for child in self._app_comments_box.get_children():
- if not isinstance(child, CommentBox):
- child.destroy()
-
- def __clean_my_non_comment_boxes(self):
- """
- Remove children that are not CommentBox objects from
- self._app_my_comments_box
- """
- for child in self._app_my_comments_box.get_children():
- if not isinstance(child, CommentBox):
- child.destroy()
-
- def __clean_non_image_boxes(self):
- """
- Remove children that are not ImageBox objects from
- self._app_images_box
- """
- for child in self._app_images_box.get_children():
- if not isinstance(child, ImageBox):
- child.destroy()
-
- def _append_comments(self, downloader, app, comments, has_more):
- """
- Append given Entropy WebService Document objects to
- the comment area.
- """
- self.__clean_non_comment_boxes()
- # make sure we didn't leave stuff here as well
- self.__clean_my_non_comment_boxes()
-
- if not comments:
- self.__show_no_comments()
- return
-
- if has_more:
- button_box = Gtk.HButtonBox()
- button = Gtk.Button()
- button.set_label(_("Older comments"))
- button.set_alignment(0.5, 0.5)
- def _enqueue_download(widget):
- widget.get_parent().destroy()
- spinner = Gtk.Spinner()
- spinner.set_size_request(24, 24)
- spinner.set_tooltip_text(_("Loading older comments..."))
- spinner.set_name("comment-box-spinner")
- self._app_comments_box.pack_end(spinner, False, False, 3)
- spinner.show()
- spinner.start()
- downloader.enqueue_download()
- button.connect("clicked", _enqueue_download)
-
- button_box.pack_start(button, False, False, 0)
- self._app_comments_box.pack_start(button_box, False, False, 1)
- button_box.show_all()
-
- idx = 0
- length = len(comments)
- # can be None
- webserv = self._entropy_ws.get(app.get_details().channelname)
- for doc in comments:
- idx += 1
- box = CommentBox(
- self._nc, self._avc, webserv, doc,
- is_last=(not has_more and (idx == length)))
- box.connect("destroy", self._on_comment_box_destroy)
- box.render()
- self._app_comments_box.pack_end(box, False, False, 2)
- box.show()
-
- def _append_comments_safe(self, downloader, app, comments, has_more):
- """
- Same as _append_comments() but thread-safe.
- """
- GLib.idle_add(self._append_comments, downloader, app,
- comments, has_more)
-
- def _append_images(self, downloader, app, images, has_more):
- """
- Append given Entropy WebService Document objects to
- the images area.
- """
- self.__clean_non_image_boxes()
-
- if not images:
- label = Gtk.Label()
- label.set_markup(
- prepare_markup(
- _("No images for this Application, yet!")))
- self._app_images_box.pack_start(label, False, False, 1)
- label.show()
- return
-
- if has_more:
- button_box = Gtk.HButtonBox()
- button = Gtk.Button()
- button.set_label(_("Older images"))
- button.set_alignment(0.5, 0.5)
- def _enqueue_download(widget):
- widget.get_parent().destroy()
- spinner = Gtk.Spinner()
- spinner.set_size_request(24, 24)
- spinner.set_tooltip_text(_("Loading older images..."))
- spinner.set_name("image-box-spinner")
- self._app_images_box.pack_end(spinner, False, False, 3)
- spinner.show()
- spinner.start()
- downloader.enqueue_download()
- button.connect("clicked", _enqueue_download)
-
- button_box.pack_start(button, False, False, 0)
- self._app_images_box.pack_start(button_box, False, False, 1)
- button_box.show_all()
-
- idx = 0
- length = len(images)
- for doc in images:
- idx += 1
- box = ImageBox(doc, is_last=(not has_more and (idx == length)))
- box.render()
- self._app_images_box.pack_end(box, False, False, 2)
- box.show()
-
- def _append_images_safe(self, downloader, app, comments, has_more):
- """
- Same as _append_images() but thread-safe.
- """
- GLib.idle_add(self._append_images, downloader, app,
- comments, has_more)
-
- def _on_app_remove(self, widget, app):
- """
- Remove the given Application.
- """
- self.emit("application-request-action",
- app, AppActions.REMOVE)
-
- def _on_app_install(self, widget, app):
- """
- Install (or reinstall) the given Application.
- """
- self.emit("application-request-action",
- app, AppActions.INSTALL)
-
- def _setup_buttons(self, app, is_installed, is_updatable):
- """
- Setup Application View Buttons (Install/Remove/Update).
- """
- button_area = self._app_button_area
- for child in button_area.get_children():
- child.destroy()
-
- if is_installed:
- if is_updatable:
- update_button = Gtk.Button()
- update_button.set_label(
- escape_markup(_("Update")))
- def _on_app_update(widget):
- return self._on_app_install(widget, app)
- update_button.connect("clicked", _on_app_update)
- button_area.pack_start(update_button, False, False, 0)
- else:
- reinstall_button = Gtk.Button()
- reinstall_button.set_label(
- escape_markup(_("Reinstall")))
- def _on_app_reinstall(widget):
- return self._on_app_install(widget, app)
- reinstall_button.connect("clicked", _on_app_reinstall)
- button_area.pack_start(reinstall_button, False, False, 0)
-
- remove_button = Gtk.Button()
- remove_button.set_label(
- escape_markup(_("Remove")))
- def _on_app_remove(widget):
- return self._on_app_remove(widget, app)
- remove_button.connect("clicked", _on_app_remove)
- button_area.pack_start(remove_button, False, False, 0)
-
- else:
- install_button = Gtk.Button()
- install_button.set_label(
- escape_markup(_("Install")))
- def _on_app_install(widget):
- return self._on_app_install(widget, app)
- install_button.connect("clicked", _on_app_install)
- button_area.pack_start(install_button, False, False, 0)
-
- button_area.show_all()
-
- def _setup_application_stats(self, stats, icon):
- """
- Setup widgets related to Application statistics (and icon).
- """
- total_downloads = stats.downloads_total
- if total_downloads < 0:
- down_msg = escape_markup(_("Not available"))
- elif not total_downloads:
- down_msg = escape_markup(_("Never downloaded"))
- else:
- down_msg = "%s %s" % (
- stats.downloads_total_markup,
- escape_markup(_("downloads")),)
-
- self._app_downloaded_lbl.set_markup(down_msg)
- if icon:
- self._image.set_from_pixbuf(icon)
- self._stars.set_rating(stats.ratings_average)
- self._stars_alignment.show_all()
-
- def _setup_application_info(self, app, metadata):
- """
- Setup the actual UI widgets content and emit 'application-show'
- """
- self._app_name_lbl.set_markup(metadata['markup'])
- self._app_info_lbl.set_markup(metadata['info'])
-
- # install/remove/update buttons
- self._setup_buttons(
- app, metadata['is_installed'],
- metadata['is_updatable'])
-
- # only comments supported, point to the remote
- # www service for the rest
- self._app_comment_more_label.set_markup(
- "%s: %s" % (
- escape_markup(_("Want to add images, etc?")),
- escape_markup(build_application_store_url(app, "ugc")),
- escape_markup(_("click here!")),))
-
- stats = metadata['stats']
- icon = metadata['icon']
- self._setup_application_stats(stats, icon)
-
- # load application comments asynchronously
- # so at the beginning, just place a spinner
- spinner = Gtk.Spinner()
- spinner.set_size_request(-1, 48)
- spinner.set_tooltip_text(escape_markup(_("Loading comments...")))
- spinner.set_name("comment-box-spinner")
- for child in self._app_comments_box.get_children():
- child.destroy()
- self._app_comments_box.pack_start(spinner, False, False, 0)
- spinner.show()
- spinner.start()
-
- downloader = ApplicationViewController.MetadataDownloader(
- app, self, self._append_comments_safe,
- app.download_comments)
- downloader.start()
-
- downloader = ApplicationViewController.MetadataDownloader(
- app, self, self._append_images_safe,
- app.download_images)
- downloader.start()
-
- self.emit("application-show", app)
-
- class MetadataDownloader(GObject.Object):
- """
- Automated Application comments downloader.
- """
-
- def __init__(self, app, avc, callback, app_downloader_method):
- self._app = app
- self._avc = avc
- self._offset = 0
- self._callback = callback
- self._task = ParallelTask(self._download)
- self._app_downloader = app_downloader_method
-
- def start(self):
- """
- Start downloading comments and send them to callback.
- Loop over until we have more of them to download.
- """
- self._offset = 0
- self._task.start()
-
- def _download_callback(self, document_list):
- """
- Callback called by download_() once data
- is arrived from web service.
- document_list can be None!
- """
- has_more = 0
- if document_list is not None:
- has_more = document_list.has_more()
- # stash more data?
- if has_more and (document_list is not None):
- self._offset += len(document_list)
- # download() will be called externally
-
- if const_debug_enabled():
- const_debug_write(
- __name__,
- "MetadataDownloader._download_callback: %s, more: %s" % (
- document_list, has_more))
- if document_list is not None:
- const_debug_write(
- __name__,
- "MetadataDownloader._download_callback: "
- "total: %s, offset: %s" % (
- document_list.total(), document_list.offset()))
-
- self._callback(self, self._app, document_list, has_more)
-
- def reset_offset(self):
- """
- Reset Metadata download offset to 0.
- """
- self._offset = 0
-
- def get_offset(self):
- """
- Get current Metadata download offset.
- """
- return self._offset
-
- def enqueue_download(self):
- """
- Enqueue a new download, starting from current offset
- """
- self._task = ParallelTask(self._download)
- self._task.start()
-
- def _download(self):
- """
- Thread body of the initial Metadata downloader.
- """
- self._app_downloader(self._download_callback,
- offset=self._offset)
-
-
class Rigo(Gtk.Application):
class RigoHandler(object):