[rigo] another chunk of Application View code
@@ -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 |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -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;
|
||||
}
|
||||
@@ -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"><small><i>Comment this <b>Application</b></i></small></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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||