[rigo] another chunk of Application View code

This commit is contained in:
Fabio Erculiani
2012-02-26 22:36:47 +01:00
parent 550be469a1
commit 4e2882ef26
18 changed files with 1402 additions and 206 deletions
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="150"
height="52"
id="svg2"
version="1.1"
inkscape:version="0.48.2 r9819"
sodipodi:docname="appview-separator.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="10.882261"
inkscape:cy="26.150743"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1280"
inkscape:window-height="744"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Livello 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1000.3622)">
<path
sodipodi:type="arc"
style="fill:#aaaaaa;fill-opacity:1;stroke:none"
id="path2985"
sodipodi:cx="64.285713"
sodipodi:cy="45"
sodipodi:rx="20"
sodipodi:ry="20"
d="m 84.285713,45 a 20,20 0 1 1 -40,0 20,20 0 1 1 40,0 z"
transform="matrix(0.76580796,0,0,0.76580796,-23.914352,991.90084)" />
<path
sodipodi:type="arc"
style="fill:#aaaaaa;fill-opacity:1;stroke:none"
id="path2985-9"
sodipodi:cx="64.285713"
sodipodi:cy="45"
sodipodi:rx="20"
sodipodi:ry="20"
d="m 84.285713,45 a 20,20 0 1 1 -40,0 20,20 0 1 1 40,0 z"
transform="matrix(0.76580796,0,0,0.76580796,26.085648,991.90084)" />
<path
sodipodi:type="arc"
style="fill:#aaaaaa;fill-opacity:1;stroke:none"
id="path2985-9-4"
sodipodi:cx="64.285713"
sodipodi:cy="45"
sodipodi:rx="20"
sodipodi:ry="20"
d="m 84.285713,45 a 20,20 0 1 1 -40,0 20,20 0 1 1 40,0 z"
transform="matrix(0.76580796,0,0,0.76580796,76.085648,991.90084)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

+66
View File
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="150"
height="40"
id="svg2"
version="1.1"
inkscape:version="0.48.2 r9819"
sodipodi:docname="comment-separator.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="-72.515822"
inkscape:cy="26.150743"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1280"
inkscape:window-height="744"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Livello 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1012.3622)">
<path
sodipodi:type="arc"
style="fill:#aaaaaa;fill-opacity:1;stroke:none"
id="path2985"
sodipodi:cx="64.285713"
sodipodi:cy="45"
sodipodi:rx="20"
sodipodi:ry="20"
d="m 84.285713,45 a 20,20 0 1 1 -40,0 20,20 0 1 1 40,0 z"
transform="matrix(0.76580796,0,0,0.76580796,25.769489,997.90084)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

+34 -12
View File
@@ -74,22 +74,44 @@ GtkViewport {
padding: 0;
}
/* Rigo specific part */
/* our infobars have to be dark */
GtkInfoBar.info {
background-color: #1c1c1c; /* gray */
}
GtkInfoBar.error {
background-color: #770000; /* dark red */
}
GtkInfoBar.warning {
background-color: #613056; /* purple */
}
GtkInfoBar #message-area GtkLabel {
color: white;
}
GtkInfoBar #message-area-error GtkLabel {
color: yellow;
}
#rigoWindow {
background-color: white;
color: black;
}
/* our infobars have to be dark */
GtkInfoBar.info {
background-color: #1c1c1c;
}
GtkInfoBar.error {
background-color: #770000;
}
GtkInfoBar.warning {
background-color: #613056;
}
GtkInfoBar #notificationMessage {
color: white;
#rigoTextView {
color: black;
background-color: #ebddff;
}
/* Application View Comment Box */
#commentBoxTitle {
color: #613056; /* purple */
}
#commentBoxComment {
color: #345f14;
}
#commentBoxAuthor {
color: brown;
}
+221 -84
View File
@@ -2,6 +2,17 @@
<interface>
<requires lib="gtk+" version="2.24"/>
<!-- interface-naming-policy project-wide -->
<object class="GtkImage" id="appViewCommentSendImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-send-receive</property>
</object>
<object class="GtkImage" id="appViewCommentSendImage1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-send-receive</property>
</object>
<object class="GtkTextBuffer" id="appViewCommentTextBuffer"/>
<object class="GtkImage" id="rigoIcon">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -23,6 +34,7 @@
<object class="GtkVBox" id="notificationBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">1</property>
<child>
<placeholder/>
</child>
@@ -99,72 +111,160 @@
</packing>
</child>
<child>
<object class="GtkVBox" id="appViewVbox">
<property name="can_focus">False</property>
<object class="GtkScrolledWindow" id="appViewScrollWin">
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<object class="GtkHBox" id="appViewHeaderHbox">
<object class="GtkViewport" id="appViewVport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<object class="GtkVBox" id="appViewStatsVbox">
<object class="GtkVBox" id="appViewVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<object class="GtkImage" id="appViewImage">
<object class="GtkHBox" id="appViewHeaderHbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="stock">gtk-missing-image</property>
<property name="icon-size">6</property>
<child>
<object class="GtkVBox" id="appViewStatsVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<object class="GtkImage" id="appViewImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="stock">gtk-missing-image</property>
<property name="icon-size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">8</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="appViewAppInfoVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="appViewNameLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<property name="label" translatable="yes">Application markup</property>
<property name="wrap">True</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="appViewInnerStatsVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkVBox" id="appViewStarsSelVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="appViewDownloadedLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Downloaded times</property>
<property name="use_markup">True</property>
<property name="justify">right</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">5</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">8</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="appViewAppInfoVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="appViewNameLabel">
<object class="GtkLabel" id="appViewInfoLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<property name="label" translatable="yes">Application markup</property>
<property name="ypad">5</property>
<property name="label" translatable="yes">Information label</property>
<property name="wrap">True</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="appViewInnerStatsVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkVBox" id="appViewStarsSelVbox">
<object class="GtkImage" id="appViewSeparatorImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">64</property>
<property name="icon_name">appview-separator</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="appViewCommentsVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
@@ -174,68 +274,105 @@
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="appViewDownloadedLabel">
<object class="GtkVBox" id="appViewCommentsWriteVbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Downloaded times</property>
<property name="use_markup">True</property>
<property name="wrap">True</property>
<child>
<object class="GtkLabel" id="appViewCommentWriteLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;small&gt;&lt;i&gt;Comment this &lt;b&gt;Application&lt;/b&gt;&lt;/i&gt;&lt;/small&gt;</property>
<property name="use_markup">True</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="appViewCommentTextAlign">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xscale">0.30000001192092896</property>
<property name="yscale">0.30000001192092896</property>
<property name="top_padding">5</property>
<property name="bottom_padding">5</property>
<child>
<object class="GtkTextView" id="appViewCommentText">
<property name="width_request">300</property>
<property name="height_request">64</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">word</property>
<property name="buffer">appViewCommentTextBuffer</property>
<property name="accepts_tab">False</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHButtonBox" id="appViewCommentButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="appViewCommentSendButton">
<property name="label" translatable="yes">_Send</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="image">appViewCommentSendImage1</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">4</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">6</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkImage" id="appViewSeparatorImage2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">64</property>
<property name="icon_name">appview-separator</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="appViewDownloadSizeLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Download size</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">5</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">10</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="appViewInfoLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<property name="label" translatable="yes">Information label</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
+305 -64
View File
@@ -27,7 +27,7 @@ from threading import Semaphore, Lock
from gi.repository import GObject
from entropy.const import const_debug_write, const_debug_enabled, \
const_convert_to_unicode
const_convert_to_unicode, etpConst
from entropy.i18n import _
from entropy.misc import ParallelTask
from entropy.services.client import WebService
@@ -168,6 +168,7 @@ class ApplicationMetadata(object):
ws_map = {}
for item in local_queue:
webserv, key, repo_id, cb, ts = item
obj = pkg_key_map.setdefault((key, repo_id), [])
obj.append((cb, ts))
@@ -181,31 +182,37 @@ class ApplicationMetadata(object):
webserv = ws_map[repo_id]
request_map = {
"vote": webserv.get_votes,
"down": webserv.get_downloads,
"icon": webserv.get_icons,
"vote": (webserv.get_votes, {}),
"down": (webserv.get_downloads, {}),
# for these two, service_cache is broken
# and actually useless.
"icon": (webserv.get_icons, {}),
"comment": (webserv.get_comments, {}),
}
# FIXME: lxnay, who validates this cache?
for request in request_list:
outcome = {}
uncached_keys = []
for key in keys:
request_func, req_kwargs = request_map[request]
try:
outcome[(key, repo_id)] = request_map[request](
[key], cache=True, cached=True)[key]
outcome[(key, repo_id)] = request_func(
[key], cache=True, cached=True,
**req_kwargs)[key]
except cache_miss:
uncached_keys.append(key)
for key in uncached_keys:
if discard_signal.get():
break
request_func, req_kwargs = request_map[request]
try:
# FIXME, lxnay: work more instances in parallel?
outcome[(key, repo_id)] = request_map[request](
[key], cache = True)[key]
outcome[(key, repo_id)] = request_func(
[key], cache = True, **req_kwargs)[key]
except ws_exception as wse:
const_debug_write(
__name__,
@@ -280,7 +287,7 @@ class ApplicationMetadata(object):
@staticmethod
def _enqueue_rating(webservice, package_key, repository_id, callback):
"""
Enqueue the retriveal of the Rating for package key in given repository.
Enqueue the retrieval of the Rating for package key in given repository.
Once the data is ready, callback() will be called passing the
payload.
This method is asynchronous and returns as soon as possible.
@@ -335,8 +342,8 @@ class ApplicationMetadata(object):
Return the Rating (stars) for given package key, if it's available
in local cache. At the same time, if not available and not already
enqueued for download, do it, atomically.
Return None if not available, the rating otherwise (tuple composed
by (vote, number_of_downloads)).
Raise WebService.CacheMiss if not available, the rating otherwise
(tuple composed by (vote, number_of_downloads)).
"""
webserv = entropy_ws.get(repository_id)
if webserv is None:
@@ -365,6 +372,48 @@ class ApplicationMetadata(object):
return vote, down
@staticmethod
def download_rating_async(entropy_ws, package_key, repository_id,
callback):
"""
Asynchronously download updated information regarding the
Rating of given package.
This request disables local cache usage and directly queries
the remote Web Service.
Once data is available, callback will be called passing the returned
payload as argument.
For this method, the signature of callback is:
callback((vote, Number_of_downloads)) [one argument, which is a tuple]
If the Web Service is not available for repository, None is passed as
payload of callback.
Please note that the callback is called from another thread.
"""
webserv = entropy_ws.get(repository_id)
if webserv is None:
task = ParallelTask(callback, None)
task.name = "DownloadRatingAsync::None"
task.daemon = True
task.start()
return None
def _getter():
outcome = None
try:
vote = webserv.get_votes(
[package_key], cache=False)[package_key]
down = webserv.get_downloads(
[package_key], cache=False)[package_key]
outcome = (vote, down)
finally:
# ignore exceptions, if any, and always
# call callback.
callback(outcome)
task = ParallelTask(_getter)
task.name = "DownloadRatingAsync::Getter"
task.daemon = True
task.start()
@staticmethod
def lazy_get_icon(entropy_ws, package_key, repository_id,
callback=None):
@@ -375,6 +424,9 @@ class ApplicationMetadata(object):
Return None if not available, or DocumentList (see Entropy Services
API) otherwise. DocumentList contains a list of Document objects,
and calling Document.local_document() would give you the image path.
If cache is not available however, WebService.CacheMiss is raised and
an asynchronous request shall be submitted to the remove web service,
once data is ready, callback will be called.
"""
webserv = entropy_ws.get(repository_id)
if webserv is None:
@@ -430,6 +482,100 @@ class ApplicationMetadata(object):
return icon
@staticmethod
def download_icon_async(entropy_ws, package_key, repository_id,
callback):
"""
Asynchronously download updated information regarding the
Icon of given package.
This request disables local cache usage and directly queries
the remote Web Service.
Once data is available, callback will be called passing the returned
payload as argument.
For this method, the signature of callback is:
callback(Icon object)
If the Web Service is not available for repository, None is passed as
payload of callback.
Please note that the callback is called from another thread.
"""
webserv = entropy_ws.get(repository_id)
if webserv is None:
task = ParallelTask(callback, None)
task.name = "DownloadIconAsync::None"
task.daemon = True
task.start()
return None
def _pick_icon(icons):
return icons[0]
def _getter():
outcome = None
try:
icons = webserv.get_icons(
[package_key], cache=False)[package_key]
# pick the first icon as document icon
icon = _pick_icon(icons)
# check if we have the file on-disk, otherwise
# spawn the fetch in parallel.
icon_path = icon.local_document()
if not os.path.isfile(icon_path):
local_path = ApplicationMetadata._download_document(
webserv, icon)
if local_path:
outcome = icon
finally:
# ignore exceptions, if any, and always
# call callback.
callback(outcome)
task = ParallelTask(_getter)
task.name = "DownloadIconAsync::Getter"
task.daemon = True
task.start()
@staticmethod
def download_comments_async(entropy_ws, package_key, repository_id,
offset, callback):
"""
Asynchronously download updated information regarding the
comments of given package.
This request disables local cache usage and directly queries
the remote Web Service.
Once data is available, callback will be called passing the returned
payload as argument.
For this method, the signature of callback is:
callback(DocumentList)
If the Web Service is not available for repository, None is passed as
payload of callback.
Please note that the callback is called from another thread.
"""
webserv = entropy_ws.get(repository_id)
if webserv is None:
task = ParallelTask(callback, None)
task.name = "DownloadCommentsAsync::None"
task.daemon = True
task.start()
return None
def _getter():
outcome = None
try:
outcome = webserv.get_comments(
[package_key], cache=False,
offset=offset)[package_key]
finally:
# ignore exceptions, if any, and always
# call callback.
callback(outcome)
task = ParallelTask(_getter)
task.name = "DownloadCommentsAsync::Getter"
task.daemon = True
task.start()
class SignalBoolean(object):
def __init__(self, val):
@@ -454,7 +600,7 @@ class ApplicationMetadata(object):
_RATING_LOCK = Lock()
_RATING_IN_FLIGHT = set()
# Application Documents logic
# Application Icons logic
_ICON_QUEUE = deque()
def _icon_thread_body_wrapper():
return ApplicationMetadata._icon_thread_body()
@@ -622,6 +768,63 @@ class Application(object):
)
return text
def get_info_markup(self):
"""
Get Application info markup text.
"""
lic_url = "%s/license/" % (etpConst['packages_website_url'],)
repo = self._entropy.open_repository(self._repo_id)
licenses = repo.retrieveLicense(self._pkg_id)
if licenses:
licenses_txt = "<b>%s</b>: " % (_("License"),)
licenses_txt += ", ".join(sorted([
"<a href=\"%s%s\">%s</a>" % (lic_url, x, x) \
for x in licenses.split()]))
else:
licenses_txt = ""
required_space = repo.retrieveOnDiskSize(self._pkg_id)
if required_space is None:
required_space = 0
required_space_txt = "<b>%s</b>: %s" % (
_("Required space"),
entropy.tools.bytes_into_human(required_space),)
down_size = repo.retrieveSize(self._pkg_id)
if down_size is None:
down_size = 0
down_size_txt = "<b>%s</b>: %s" % (
_("Download size"),
entropy.tools.bytes_into_human(down_size),)
digest = repo.retrieveDigest(self._pkg_id)
if digest is None:
digest = _("N/A")
digest_txt = "<b>%s</b>: %s" % (_("Checksum"), digest)
uses = repo.retrieveUseflags(self._pkg_id)
uses = sorted(uses)
use_list = []
use_url = "%s/useflag/" % (etpConst['packages_website_url'],)
for use in uses:
txt = "<a href=\"%s%s\">%s</a>" % (use_url, use, use)
use_list.append(txt)
use_txt = "<b>%s</b>: " % (_("USE flags"),)
if use_list:
use_txt += " ".join(use_list)
else:
use_txt += _("No use flags")
text = "<small>%s\n%s\n%s\n%s\n%s</small>" % (
down_size_txt,
required_space_txt,
licenses_txt,
digest_txt,
use_txt,
)
return text
def get_review_stats(self):
"""
Return ReviewStats object containing user review
@@ -655,6 +858,82 @@ class Application(object):
stat.downloads_total = down
return stat
def get_icon(self):
"""
Return Application Icon image Entropy Document object.
In case of missing icon, None is returned.
The actual outcome of this property is a tuple, composed
by the Document object (or None) and cache hit information
(True if got from local cache, False if not in local cache)
"""
repo = self._entropy.open_repository(self._repo_id)
key_slot = repo.retrieveKeySlot(self._pkg_id)
if key_slot is None:
return None, False
key, slot = key_slot
cache_hit = True
try:
icon = ApplicationMetadata.lazy_get_icon(
self._entropy_ws, key, self._repo_id,
callback=self._redraw_callback)
except WebService.CacheMiss:
cache_hit = False
icon = None
if const_debug_enabled():
const_debug_write(__name__,
"Application{%s}.get_icon: icon: %s, cache hit: %s" % (
self._pkg_match,
icon, cache_hit))
return icon, cache_hit
def download_comments(self, callback, offset=0):
"""
Return Application Comments Entropy Document object.
In case of missing comments (locally), None is returned.
The actual outcome of this method is a DocumentList object.
"""
repo = self._entropy.open_repository(self._repo_id)
key_slot = repo.retrieveKeySlot(self._pkg_id)
if key_slot is None:
task = ParallelTask(callback, None)
task.name = "DownloadCommentsNoneCallback"
task.daemon = True
task.start()
return
key, slot = key_slot
ApplicationMetadata.download_comments_async(
self._entropy_ws, key, self._repo_id,
offset, callback)
if const_debug_enabled():
const_debug_write(__name__,
"Application{%s}.download_comments called" % (
self._pkg_match,))
def is_webservice_available(self):
"""
Return whether the Entropy Web Service is available
for this Application.
"""
webserv = self._entropy_ws.get(self._repo_id)
if webserv:
return True
return False
def get_webservice_username(self):
"""
Return the currently logged Entropy Web Services
username, or None, if any.
"""
webserv = self._entropy_ws.get(self._repo_id)
if webserv is None:
return None
return webserv.get_credentials()
# get a AppDetails object for this Applications
def get_details(self):
"""
@@ -711,38 +990,6 @@ class AppDetails(object):
def error(self):
return _("Application not found")
@property
def icon(self):
"""
Return Application Icon image Entropy Document object.
In case of missing icon, None is returned.
The actual outcome of this property is a tuple, composed
by the Document object (or None) and cache hit information
(True if got from local cache, False if not in local cache)
"""
repo = self._entropy.open_repository(self._repo_id)
key_slot = repo.retrieveKeySlot(self._pkg_id)
if key_slot is None:
return None, False
key, slot = key_slot
cache_hit = True
try:
icon = ApplicationMetadata.lazy_get_icon(
self._entropy_ws, key, self._repo_id,
callback=self._redraw_callback)
except WebService.CacheMiss:
cache_hit = False
icon = None
if const_debug_enabled():
const_debug_write(__name__,
"AppDetails{%s}.icon: icon: %s, cache hit: %s" % (
self._pkg_match,
icon, cache_hit))
return icon, cache_hit
@property
def installation_date(self):
"""
@@ -817,9 +1064,19 @@ class AppDetails(object):
@property
def pkgname(self):
"""
Return un-mangled package name belonging to this Application.
Return unmangled package name belonging to this Application.
"""
return self.name
repo = self._entropy.open_repository(self._repo_id)
return repo.retrieveName(self._pkg_id)
@property
def pkgkey(self):
"""
Return unmangled package key name belonging to this Application.
"""
repo = self._entropy.open_repository(self._repo_id)
key, slot = repo.retrieveKeySlot(self._pkg_id)
return key
@property
def signing_key_id(self):
@@ -844,19 +1101,3 @@ class AppDetails(object):
repo = self._entropy.open_repository(self._repo_id)
return repo.retrieveHomepage(self._pkg_id)
def __str__(self):
details = []
details.append("* AppDetails")
details.append(" name: %s" % self.name)
details.append(" display_name: %s" % self.display_name)
details.append(" pkg: %s" % self.pkg)
details.append(" pkgname: %s" % self.pkgname)
details.append(" channelname: %s" % self.channelname)
details.append(" description: %s" % self.description)
details.append(" error: %s" % self.error)
details.append(" icon: %s" % self.icon)
details.append(" installation_date: %s" % self.installation_date)
details.append(" licenses: %s" % self.licenses)
details.append(" version: %s" % self.version)
details.append(" website: %s" % self.website)
return '\n'.join(details)
+7 -2
View File
@@ -74,11 +74,16 @@ class AppListStore(Gtk.ListStore):
def _ui_redraw_callback(*args):
if const_debug_enabled():
const_debug_write(__name__,
"_ui_redraw_callback()")
"_ui_redraw_callback(), %s" % (args,))
self.emit("redraw-request", pkg_match)
app = Application(self._entropy, self._entropy_ws, pkg_match,
redraw_callback=_ui_redraw_callback)
icon, cache_hit = app.get_details().icon
icon, cache_hit = app.get_icon()
if const_debug_enabled():
const_debug_write(__name__,
"get_icon({%s, %s}) = %s, hit: %s" % (
(pkg_match, app.name, icon, cache_hit,)))
if icon is None:
if cache_hit:
# this means that there is no icon for package
+84
View File
@@ -0,0 +1,84 @@
# 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, Pango, GObject
from entropy.client.services.interfaces import Document, DocumentFactory
import entropy.tools
class CommentBox(Gtk.VBox):
def __init__(self, comment, is_last=False):
Gtk.VBox.__init__(self)
self._comment = comment
self.set_name("commentBox")
self.set_spacing(2)
self._is_last = is_last
def render(self):
# title, keywords, ddata, document_id
title = self._comment['title'].strip()
if title:
title_id = Document.DOCUMENT_TITLE_ID
label = Gtk.Label()
label.set_markup("<b>" + self._comment[title_id] + "</b>")
label.set_name("commentBoxTitle")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 0.0)
label.set_selectable(True)
label.show()
self.pack_start(label, False, False, 0)
data_id = Document.DOCUMENT_DATA_ID
label = Gtk.Label()
label.set_markup("<small>" + self._comment[data_id] + "</small>")
label.set_name("commentBoxComment")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 0.0)
label.set_selectable(True)
label.show()
self.pack_start(label, False, False, 0)
ts_id = Document.DOCUMENT_TIMESTAMP_ID
user_id = DocumentFactory.DOCUMENT_USERNAME_ID
label = Gtk.Label()
time_str = entropy.tools.convert_unix_time_to_human_time(
self._comment[ts_id])
time_str = GObject.markup_escape_text(time_str)
label.set_markup(
"<small><i>" + time_str \
+ "</i>, <b>%s</b>" % (self._comment[user_id],) \
+ "</small>")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.98, 0.0)
label.set_selectable(True)
label.set_name("commentBoxAuthor")
label.show()
self.pack_start(label, False, False, 0)
if not self._is_last:
image = Gtk.Image.new_from_icon_name("comment-separator",
Gtk.IconSize.BUTTON)
image.set_pixel_size(50)
self.pack_start(image, False, False, 0)
image.show()
+149 -21
View File
@@ -1,7 +1,9 @@
from gi.repository import Gtk, GObject
from gi.repository import Gtk, GLib, GObject
from entropy.i18n import _
from entropy.services.client import WebService
from entropy.misc import ParallelTask
class NotificationBox(Gtk.HBox):
@@ -11,9 +13,12 @@ class NotificationBox(Gtk.HBox):
Rigo notification area.
"""
def __init__(self, message, message_type=None, tooltip=None):
def __init__(self, message, message_widget=None,
message_type=None, tooltip=None):
Gtk.HBox.__init__(self)
self._message = message
# if not None, it will replace Gtk.Label(self._message)
self._message_widget = message_widget
self._buttons = []
self._type = message_type
if self._type is None:
@@ -31,6 +36,14 @@ class NotificationBox(Gtk.HBox):
self._buttons.append(button)
return button
def add_destroy_button(self, text):
"""
Add button that destroys the whole Notification object.
"""
def _destroy(*args):
self.destroy()
self.add_button(text, _destroy)
def render(self):
"""
Render the Notification box filling in the container.
@@ -42,25 +55,30 @@ class NotificationBox(Gtk.HBox):
content_area = bar.get_content_area()
hbox = Gtk.HBox()
label = Gtk.Label()
label.set_markup(self._message)
# make it css-able
label.set_name("notificationMessage")
label.set_property("expand", True)
label.set_alignment(0.02, 0.50)
hbox.pack_start(label, True, True, 0)
label.show()
message_hbox = Gtk.HBox()
message_hbox.set_name("message-area")
if self._message_widget is None:
label = Gtk.Label()
label.set_markup(self._message)
# make it css-able
label.set_property("expand", True)
label.set_alignment(0.02, 0.50)
message_hbox.pack_start(label, True, True, 0)
else:
message_hbox.pack_start(self._message_widget, True, True, 0)
hbox.pack_start(message_hbox, True, True, 0)
button_hbox = Gtk.HBox()
button_hbox.set_name("button-area")
for button in self._buttons:
hbox.pack_start(button, False, False, 3)
button.show()
button_hbox.pack_start(button, False, False, 3)
hbox.pack_start(button_hbox, False, False, 2)
content_area.set_property("expand", False)
content_area.add(hbox)
content_area.show()
hbox.show()
bar.show()
bar.show_all()
bar.get_action_area().hide()
self.pack_start(bar, True, True, 0)
@@ -103,9 +121,7 @@ class UpdatesNotificationBox(NotificationBox):
message_type=Gtk.MessageType.WARNING)
self.add_button(_("_Update System"), self._update)
self.add_button(_("_Show"), self._show)
def _destroy(*args):
self.destroy()
self.add_button(_("_Ignore"), _destroy)
self.add_destroy_button(_("_Ignore"))
def _update(self, button):
"""
@@ -140,12 +156,124 @@ class RepositoriesUpdateNotificationBox(NotificationBox):
tooltip=_("I dunno dude, I'd say Yes"),
message_type=Gtk.MessageType.ERROR)
self.add_button(_("_Yes, why not?"), self._update)
def _destroy(*args):
self.destroy()
self.add_button(_("_No, thanks"), _destroy)
self.add_destroy_button(_("_No, thanks"))
def _update(self, button):
"""
Update button callback from the updates notification box.
"""
self.emit("update-request")
class LoginNotificationBox(NotificationBox):
"""
NotificationBox asking user to login to Entropy Web Service.
"""
__gsignals__ = {
# Emitted when login is successful
"login-success" : (GObject.SignalFlags.RUN_LAST,
None,
(GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT,),
),
# Emitted when login fails, not going
# to retry anymore
"login-failed" : (GObject.SignalFlags.RUN_LAST,
None,
(GObject.TYPE_PYOBJECT,),
),
}
def __init__(self, entropy_ws, app):
self._entropy_ws = entropy_ws
self._app = app
self._repository_id = app.get_details().channelname
NotificationBox.__init__(self, None,
message_widget=self._make_login_box(),
tooltip=_("You need to login to Entropy Web Services"),
message_type=Gtk.MessageType.WARNING)
self.add_button(_("_Login"), self._login)
def _destroy(*args):
self.emit("login-failed", self._app)
self.destroy()
self.add_button(_("_Cancel"), _destroy)
def _make_login_box(self):
vbox = Gtk.VBox()
hbox = Gtk.HBox()
username_label = Gtk.Label()
username_label.set_markup(_("Username:"))
hbox.pack_start(username_label, False, False, 2)
self._username_entry = Gtk.Entry()
hbox.pack_start(self._username_entry, False, False, 0)
password_label = Gtk.Label()
password_label.set_markup(_(", password:"))
hbox.pack_start(password_label, False, False, 2)
self._password_entry = Gtk.Entry()
self._password_entry.set_visibility(False)
hbox.pack_start(self._password_entry, False, False, 0)
hbox.set_property("expand", True)
vbox.pack_start(hbox, False, False, 0)
self._login_message = Gtk.Label()
self._login_message.set_no_show_all(True)
self._login_message.set_alignment(0.0, 1.0)
self._login_message.set_padding(-1, 8)
self._login_message.set_name("message-area-error")
vbox.pack_start(self._login_message, False, False, 0)
return vbox
def _login_thread_body(self, username, password):
"""
Execute the actual login procedure.
"""
webserv = self._entropy_ws.get(self._repository_id)
if webserv is None:
# can't be, if we're here, this is already not None
return
def _login_error():
self._login_message.show()
self._login_message.set_markup(
_("Login <b>error</b>!"))
webserv.add_credentials(username, password)
try:
webserv.validate_credentials()
except WebService.MethodNotAvailable:
GLib.idle_add(_login_error)
return
except WebService.AuthenticationFailed:
webserv.remove_credentials()
GLib.idle_add(_login_error)
return
def _emit_success():
self.emit("login-success", username, self._app)
GLib.idle_add(self.destroy)
GLib.idle_add(_emit_success)
def _login(self, button):
"""
Try to login to Entropy Web Services.
"""
username = self._username_entry.get_text()
password = self._password_entry.get_text()
task = ParallelTask(
self._login_thread_body, username, password)
task.name = "LoginNotificationThreadBody"
task.daemon = True
task.start()
+35
View File
@@ -0,0 +1,35 @@
import os
from entropy.const import etpConst
from entropy.core.settings.base import SystemSettings
def build_application_store_url(app, sub_page):
"""
take rigo.models.application.Application object
and build up HTTP Entropy Application Store URL
pointing to exact Application page.
sub_page is used to load a specific part of the page,
for example "ugc" can be passed to load URL/ugc page.
"""
settings = SystemSettings()
details = app.get_details()
pkg_id, pkg_repo = details.pkg
branch = settings['repositories']['branch']
product = settings['repositories']['product']
url = "%s/show/%s,%s,%s,%s,%s,%s/%s" % (
etpConst['packages_website_url'],
details.pkgname,
pkg_id,
pkg_repo,
etpConst['currentarch'],
branch,
product,
sub_page)
return url
def build_register_url():
"""
Build User Account Registration Form URL.
"""
return os.path.join(etpConst['distro_website_url'], "register")
+415 -23
View File
@@ -21,15 +21,20 @@ from rigo.entropyapi import EntropyWebService
from rigo.models.application import Application, ApplicationMetadata
from rigo.ui.gtk3.widgets.apptreeview import AppTreeView
from rigo.ui.gtk3.widgets.notifications import NotificationBox, \
RepositoriesUpdateNotificationBox, UpdatesNotificationBox
RepositoriesUpdateNotificationBox, UpdatesNotificationBox, \
LoginNotificationBox
from rigo.ui.gtk3.widgets.welcome import WelcomeBox
from rigo.ui.gtk3.widgets.stars import ReactiveStar
from rigo.ui.gtk3.widgets.comments import CommentBox
from rigo.ui.gtk3.models.appliststore import AppListStore
from rigo.ui.gtk3.utils import init_sc_css_provider, get_sc_icon_theme
from rigo.utils import build_application_store_url, build_register_url
from entropy.const import etpUi, const_debug_write, const_debug_enabled
from entropy.const import etpUi, const_debug_write, const_debug_enabled, \
const_convert_to_unicode
from entropy.client.interfaces import Client
from entropy.client.interfaces.repository import Repository
from entropy.services.client import WebService
from entropy.misc import TimeScheduled, ParallelTask
from entropy.i18n import _, ngettext
@@ -151,7 +156,10 @@ class ApplicationsViewController(GObject.Object):
class NotificationViewController(GObject.Object):
"""
Notification area widget controller code.
Notification area widget controller.
This class features the handling of some built-in
Notification objects (like updates and outdated repositories)
but also accepts external NotificationBox instances as well.
"""
def __init__(self, entropy_client, avc, notification_box):
@@ -290,17 +298,22 @@ class ApplicationViewController(GObject.Object):
# Double click on application widget
"application-activated" : (GObject.SignalFlags.RUN_LAST,
None,
(GObject.TYPE_PYOBJECT, ),
(GObject.TYPE_PYOBJECT,),
),
# Show Application in the Rigo UI
"application-show" : (GObject.SignalFlags.RUN_LAST,
None,
(GObject.TYPE_PYOBJECT, ),
(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, ),
(GObject.TYPE_PYOBJECT,),
),
# action requested for application
"application-request-action" : (GObject.SignalFlags.RUN_LAST,
@@ -318,15 +331,22 @@ class ApplicationViewController(GObject.Object):
self._entropy = entropy_client
self._entropy_ws = entropy_ws
self._app_store = None
self._last_pkg_match = None
self._last_app = None
self._nc = None
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_download_lbl = self._builder.get_object(
"appViewDownloadedLabel")
self._app_downloaded_lbl = self._builder.get_object(
"appViewDownloadSizeLabel")
"appViewDownloadedLabel")
self._app_comments_box = self._builder.get_object("appViewCommentsVbox")
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("rigoTextView")
self._app_comment_text_buffer = self._builder.get_object(
"appViewCommentTextBuffer")
self._stars_container = self._builder.get_object("appViewStarsSelVbox")
self._stars = ReactiveStar()
@@ -337,6 +357,11 @@ class ApplicationViewController(GObject.Object):
self._stars_container.pack_start(self._stars_alignment, False, False, 0)
def set_notification_controller(self, nc):
"""
Bind NotificationController object to this class.
"""
self._nc = nc
def set_store(self, store):
"""
@@ -347,6 +372,18 @@ class ApplicationViewController(GObject.Object):
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)
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):
"""
@@ -354,7 +391,7 @@ class ApplicationViewController(GObject.Object):
information. Once we're done loading the shit, we just emit
'application-show' and let others do the UI switch.
"""
self._last_pkg_match = app.get_details().pkg
self._last_app = app
task = ParallelTask(self.__application_activate, app)
task.name = "ApplicationActivate"
task.daemon = True
@@ -365,7 +402,9 @@ class ApplicationViewController(GObject.Object):
Redraw request received from AppListStore for given package match.
We are required to update rating, number of downloads, icon.
"""
if pkg_match == self._last_pkg_match:
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)
@@ -377,6 +416,7 @@ class ApplicationViewController(GObject.Object):
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
@@ -385,6 +425,256 @@ class ApplicationViewController(GObject.Object):
metadata['icon'] = self._app_store.get_icon(details.pkg)
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
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="utf-8")
# get entropy ws, check if we're logged in
# if not, use LoginNotificationBox and wait on signal
# if yes, use SubmitDocumentNotificationBox and wait on signal
# FIXME, lxnay complete
def _sender(app, text):
if not app.is_webservice_available():
GLib.idle_add(self._notify_webservice_na, app)
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)
task = ParallelTask(_sender, app, text)
task.name = "AppViewSendComment"
task.start()
def _notify_webservice_na(self, app):
"""
Notify Web Service unavailability for given Application object.
"""
box = NotificationBox(
"%s: <b>%s</b>" % (
_("Entropy Web Services not available for repository"),
app.get_details().channelname),
message_type=Gtk.MessageType.ERROR)
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 <b>comment</b> as <b>%s</b>.") \
% (GObject.markup_escape_text(username),),
message_type=Gtk.MessageType.INFO)
def _comment_submit(widget):
box.destroy()
self._comment_submit(app, username, text)
box.add_button(_("_Ok, cool!"), _comment_submit)
def _logout_webservice(widget):
box.destroy()
self._logout_webservice(app)
box.add_button(_("_No, logout!"), _logout_webservice)
box.add_destroy_button(_("_Abort"))
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(doc, is_last=True)
box.render()
self._app_comments_box.pack_start(box, False, False, 2)
box.show()
nbox = NotificationBox(
_("Your comment has been submitted!"),
message_type=Gtk.MessageType.INFO)
nbox.add_destroy_button(_("Ok, great!"))
self._nc.append(nbox, timeout=10)
def _submit_fail():
box = NotificationBox(
_("Comment submit error: <i>%s</i>") % (err_msg,),
message_type=Gtk.MessageType.ERROR)
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):
"""
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()
def _send_comment():
self._on_send_comment(None, app=app)
GLib.idle_add(_send_comment)
def _notify_login_request(self, app, text):
"""
Notify User that login is required
"""
box = LoginNotificationBox(self._entropy_ws, app)
box.connect("login-success", self._on_comment_login_success)
box.connect("login-failed", self._on_comment_login_failed)
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 <b>%s</b>! How about your <b>comment</b>?") \
% (GObject.markup_escape_text(username),),
message_type=Gtk.MessageType.INFO)
def _send_comment(widget):
box.destroy()
self._on_send_comment(widget, app=app)
box.add_button(_("_Send now"), _send_comment)
box.add_destroy_button(_("_Abort"))
self._nc.append(box)
def _on_comment_login_failed(self, widget, app):
"""
Entropy Web Services Login failed message.
"""
box = NotificationBox(
_("Login failed. Your <b>comment</b> hasn't been added"),
message_type=Gtk.MessageType.ERROR)
box.add_destroy_button(_("_Ok, thanks"))
self._nc.append(box)
def _append_comments(self, downloader, app, comments, has_more):
"""
Append given Entropy WebService Document objects to
the comment area.
"""
more_button_name = "commentBoxMoreButton"
# remove spinner if there, ugly O(n)
for child in self._app_comments_box.get_children():
if isinstance(child, Gtk.Spinner):
child.destroy()
if child.get_name() == more_button_name:
child.destroy()
if not comments:
label = Gtk.Label()
label.set_markup(
_("<i>No <b>comments</b> for this Application, yet!</i>"))
self._app_comments_box.pack_start(label, False, False, 1)
label = Gtk.Label()
url = build_register_url()
url = GObject.markup_escape_text(url)
reg_msg = _("You need to <a href=\"%s\">register here</a>") % (
url,)
label.set_markup("<small>" + reg_msg + "</small>")
label.show()
self._app_comments_box.pack_start(label, False, False, 1)
return
idx = 0
length = len(comments)
for doc in comments:
idx += 1
box = CommentBox(doc, is_last=(not has_more and (idx == length)))
box.render()
self._app_comments_box.pack_start(box, False, False, 2)
box.show()
if has_more:
button = Gtk.Button()
button.set_label(_("More comments"))
button.set_name(more_button_name)
button.set_alignment(0.5, 0.5)
def _enqueue_download(widget):
widget.hide()
spinner = Gtk.Spinner()
spinner.set_size_request(-1, 24)
spinner.set_tooltip_text(_("Loading comments..."))
spinner.set_name("commentBoxSpinner")
self._app_comments_box.pack_start(spinner, False, False, 3)
spinner.show()
downloader.enqueue_download()
button.connect("clicked", _enqueue_download)
self._app_comments_box.pack_start(button, False, False, 1)
button.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 _setup_application_stats(self, stats, icon):
"""
Setup widgets related to Application statistics (and icon).
@@ -393,11 +683,12 @@ class ApplicationViewController(GObject.Object):
if not total_downloads:
down_msg = _("Never downloaded")
else:
down_msg = ngettext("<small><b>%d</b> download</small>",
"<small><b>%d</b> downloads</small>",
down_msg = ngettext("<small><b>%d</b>\ndownload</small>",
"<small><b>%d</b>\ndownloads</small>",
total_downloads)
down_msg = down_msg % (total_downloads,)
self._app_download_lbl.set_markup(down_msg)
self._app_downloaded_lbl.set_markup(down_msg)
if icon:
self._image.set_from_pixbuf(icon)
self._stars.set_rating(stats.ratings_average - 1)
@@ -407,23 +698,119 @@ class ApplicationViewController(GObject.Object):
"""
Setup the actual UI widgets content and emit 'application-show'
"""
# FIXME, lxnay complete
down_size = entropy.tools.bytes_into_human(
metadata['download_size'])
self._app_downloaded_lbl.set_markup("<small>%s</small>" % (down_size,))
self._app_name_lbl.set_markup(metadata['markup'])
self._app_info_lbl.set_markup(metadata['info'])
# GPG info, required space, use flags
# FIXME, lxnay complete
# install/remove/update buttons
# show comments (and allow commenting)
# allow screenshots upload?
# for the rest, point to the remote www service
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(_("Loading comments..."))
spinner.set_name("commentBoxSpinner")
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.CommentsDownloader(
app, self, self._append_comments_safe)
downloader.start()
self.emit("application-show", app)
class CommentsDownloader(GObject.Object):
"""
Automated Application comments downloader.
"""
def __init__(self, app, avc, callback):
self._app = app
self._avc = avc
self._offset = 0
self._callback = callback
self._avc.connect("application-hide", self.stop)
self._task = ParallelTask(self._download)
self._started = False
def stop(self, *args):
"""
Stop downloading comments, if we're still doing it.
"""
self._started = False
print("Application-hide called, this is", self)
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._started = True
self._task.start()
def _download_callback(self, document_list):
"""
Callback called by download_comments() 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__,
"CommentsDownloader._download_callback: %s, more: %s" % (
document_list, has_more))
if document_list is not None:
const_debug_write(
__name__,
"CommentsDownloader._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 Comments download offset to 0.
"""
self._offset = 0
def get_offset(self):
"""
Get current Comments 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 Comments downloader.
"""
self._app.download_comments(self._download_callback,
offset=self._offset)
class Rigo(Gtk.Application):
@@ -455,7 +842,10 @@ class Rigo(Gtk.Application):
self._window.set_name("rigoWindow")
self._apps_view = self._builder.get_object("appsViewVbox")
self._scrolled_view = self._builder.get_object("appsViewScrolledWindow")
self._app_view = self._builder.get_object("appViewVbox")
self._app_view = self._builder.get_object("appViewScrollWin")
self._app_view.set_name("rigoWindow")
self._app_view_port = self._builder.get_object("appViewVport")
self._app_view_port.set_name("rigoWindow")
self._search_entry = self._builder.get_object("searchEntry")
self._static_view = self._builder.get_object("staticViewVbox")
self._notification = self._builder.get_object("notificationBox")
@@ -512,6 +902,7 @@ class Rigo(Gtk.Application):
self._nc = NotificationViewController(
self._entropy, self._avc, self._notification)
self._app_view_c.set_notification_controller(self._nc)
def _on_view_cleared(self, *args):
self._change_view_state(Rigo.STATIC_VIEW_STATE)
@@ -572,6 +963,7 @@ class Rigo(Gtk.Application):
state (or mode). Hiding back application information.
"""
self._app_view.hide()
self._app_view_c.hide()
def _change_view_state(self, state):
"""