[Sugar-devel] [PATCH] sl#3315: Journal-Entry transfer from 1-to-N users.
Ajay Garg
ajay at activitycentral.com
Fri Feb 3 01:41:13 EST 2012
---
The workflow can be assessed via the screenshots at ::
http://wiki.sugarlabs.org/go/Features/Transfer_to_many_screenshots
configure.ac | 1 +
data/icons/Makefile.am | 1 +
data/icons/module-configuration.svg | 190 +++++++++++
extensions/cpsection/Makefile.am | 1 +
extensions/cpsection/configuration/Makefile.am | 6 +
extensions/cpsection/configuration/__init__.py | 22 ++
extensions/cpsection/configuration/model.py | 21 ++
extensions/cpsection/configuration/view.py | 432 ++++++++++++++++++++++++
src/jarabe/frame/activitiestray.py | 69 ++++-
src/jarabe/journal/palettes.py | 128 +++++++-
src/jarabe/model/buddy.py | 12 +
src/jarabe/model/filetransfer.py | 18 +-
src/jarabe/model/friends.py | 267 ++++++++++++++-
src/jarabe/view/buddymenu.py | 87 +++++-
14 files changed, 1222 insertions(+), 33 deletions(-)
create mode 100644 data/icons/module-configuration.svg
create mode 100644 extensions/cpsection/configuration/Makefile.am
create mode 100644 extensions/cpsection/configuration/__init__.py
create mode 100644 extensions/cpsection/configuration/model.py
create mode 100644 extensions/cpsection/configuration/view.py
diff --git a/configure.ac b/configure.ac
index fa7165c..a70621d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -52,6 +52,7 @@ data/sugar-emulator.desktop
extensions/cpsection/aboutcomputer/Makefile
extensions/cpsection/accessibility/Makefile
extensions/cpsection/aboutme/Makefile
+extensions/cpsection/configuration/Makefile
extensions/cpsection/datetime/Makefile
extensions/cpsection/frame/Makefile
extensions/cpsection/keyboard/Makefile
diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am
index 8e01626..60f735a 100644
--- a/data/icons/Makefile.am
+++ b/data/icons/Makefile.am
@@ -4,6 +4,7 @@ sugar_DATA = \
module-about_me.svg \
module-about_my_computer.svg \
module-accessibility.svg \
+ module-configuration.svg \
module-date_and_time.svg \
module-frame.svg \
module-keyboard.svg \
diff --git a/data/icons/module-configuration.svg b/data/icons/module-configuration.svg
new file mode 100644
index 0000000..16ca355
--- /dev/null
+++ b/data/icons/module-configuration.svg
@@ -0,0 +1,190 @@
+<?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="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="module-configuration.svg">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective6637" />
+ <inkscape:perspective
+ id="perspective6615"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.35"
+ inkscape:cx="375"
+ inkscape:cy="514.28571"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1366"
+ inkscape:window-height="693"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ 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="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="layer1-3"
+ inkscape:label="Layer 1"
+ transform="matrix(2.0112611,0,0,2.8271726,-382.79436,-991.72999)">
+ <g
+ inkscape:label="Layer 1"
+ id="layer1-1"
+ transform="translate(170.0671,-314.28571)">
+ <g
+ transform="matrix(9.8137136,0,0,9.8137136,-2250.1262,598.08659)"
+ id="g6596">
+ <g
+ id="g7197">
+ <path
+ sodipodi:type="arc"
+ style="fill:none;stroke:#00000f;stroke-width:9.03419971;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path5989"
+ sodipodi:cx="307.3714"
+ sodipodi:cy="24.611456"
+ sodipodi:rx="12.380237"
+ sodipodi:ry="11.953332"
+ d="m 319.75164,24.611456 c 0,6.601643 -5.54283,11.953332 -12.38024,11.953332 -6.83742,0 -12.38024,-5.351689 -12.38024,-11.953332 0,-6.601643 5.54282,-11.953332 12.38024,-11.953332 6.83741,0 12.38024,5.351689 12.38024,11.953332 z"
+ transform="matrix(0.77926228,0,0,0.79380856,10.613376,6.1120011)" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505"
+ width="7.9117804"
+ height="9.2506971"
+ x="246.11057"
+ y="7.1083627"
+ ry="0.82999998"
+ rx="0.82999998" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505-9"
+ width="7.9117804"
+ height="9.2506971"
+ x="246.21878"
+ y="35.461403"
+ rx="0.82999998"
+ ry="0.82999998" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505-7"
+ width="7.9117804"
+ height="9.2506971"
+ x="-29.96331"
+ y="231.45294"
+ transform="matrix(0,-1,1,0,0,0)"
+ ry="0.82999998"
+ rx="0.82999998" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505-6"
+ width="7.9117804"
+ height="9.2506971"
+ x="-29.848055"
+ y="259.62869"
+ transform="matrix(0,-1,1,0,0,0)"
+ ry="0.82999998"
+ rx="0.82999998" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505-76"
+ width="7.9117804"
+ height="9.2506971"
+ x="154.41437"
+ y="176.47606"
+ transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
+ ry="0.82999998"
+ rx="0.82999998" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505-76-7"
+ width="7.9117804"
+ height="9.2506971"
+ x="-199.0881"
+ y="-177.31622"
+ transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,0,0)"
+ ry="0.82999998"
+ rx="0.82999998" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505-76-5"
+ width="7.9117804"
+ height="9.2506971"
+ x="154.6358"
+ y="204.75911"
+ transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
+ ry="0.82999998"
+ rx="0.82999998" />
+ <rect
+ style="fill:#00000f;fill-opacity:1;stroke:none"
+ id="rect6505-76-0"
+ width="7.9117804"
+ height="9.2506971"
+ x="-199.24612"
+ y="-148.95982"
+ transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,0,0)"
+ ry="0.82999998"
+ rx="0.82999998" />
+ </g>
+ </g>
+ </g>
+ <path
+ inkscape:connector-curvature="0"
+ id="path3073"
+ d="M 340.90476,719.02884 C 339.85714,717.98124 339,703.77275 339,687.4545 l 0,-29.6696 -10.32864,-4.3156 -10.32863,-4.31557 -22.38347,21.78237 c -18.44567,17.95034 -23.2886,21.29505 -27.52851,19.01228 -6.09928,-3.28383 -46.57361,-44.63814 -46.57361,-47.5862 0,-1.12068 9.16077,-11.48513 20.35725,-23.03211 l 20.35725,-20.99453 -4.31522,-10.84382 -4.31523,-10.84382 -29.61345,-1.42857 -29.61345,-1.42858 -0.80918,-34.05845 c -0.60171,-25.32665 0.13079,-34.65488 2.85715,-36.38482 2.01647,-1.2795 15.35566,-2.34605 29.64263,-2.37012 l 25.97632,-0.0437 3.54536,-11.83336 3.54537,-11.83336 -18.8074,-18.95263 c -10.34407,-10.42395 -18.8074,-20.23446 -18.8074,-21.80115 0,-1.56668 10.92551,-13.71726 24.2789,-27.00129 l 24.2789,-24.15277 19.97202,19.81898 19.97202,19.81898 12.17765,-4.4318 12.17766,-4.4318 0.71651,-24.02847 c 1.19845,-40.19036 -1.99777,-37.19155 38.64555,-36.25866 L 409,356.6479 l 0.80987,28.88986 0.80986,28.88986 11.95928,4.69773 11.95928,4.69774 18.73113,-18.5876 c 10.30212,-10.22317 20.65581,-18.58759 23.00821,-18.58759 2.3524,0 15.0678,10.84703 28.2564,24.10452 l 23.97931,24.10453 -20.52888,21.17152 -20.52889,21.17153 4.32606,8.29538 c 2.37931,4.56245 4.33394,9.85754 4.34363,11.76685 0.0114,2.44368 8.68823,3.71255 29.30331,4.28572 l 29.28572,0.81423 0,35.71429 0,35.71428 -29.28915,0.81428 -29.28914,0.81428 -4.3258,10.35309 -4.3258,10.3531 20.04351,20.67097 c 11.02395,11.36904 20.04352,22.24137 20.04352,24.16074 0,5.18526 -43.1282,48.54783 -48.28543,48.54783 -2.44054,0 -13.64577,-9.14697 -24.90053,-20.32663 l -20.46319,-20.3266 -10.31828,3.40534 -10.31829,3.40532 0,28.11617 c 0,15.46391 -0.78041,30.14991 -1.73425,32.63557 -1.44498,3.76557 -7.32266,4.5194 -35.23809,4.5194 -18.42712,0 -34.36099,-0.85714 -35.40861,-1.90477 l 0,0 z M 394.3828,591.70551 c 16.14693,-4.82459 33.99418,-23.23962 38.65434,-39.88406 7.17003,-25.60883 -3.3713,-52.07528 -26.41559,-66.32247 -11.46728,-7.08967 -16.74237,-8.49394 -31.90726,-8.49394 -25.11984,0 -42.50709,10.48723 -53.57143,32.31195 -12.59921,24.85226 -8.97102,47.7484 10.68602,67.43544 17.12848,17.15462 38.27107,22.20862 62.55392,14.95308 l 0,0 z"
+ style="fill:#a0a0a0;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3075"
+ d="m 340.31812,686.89964 -2.03213,-31.35394 -10.35728,-4.13652 -10.35728,-4.13651 -22.11154,21.11617 c -12.16135,11.61391 -23.18839,21.1162 -24.50454,21.1162 -1.31614,0 -12.4099,-10.29334 -24.65281,-22.87409 -25.3507,-26.05026 -25.50888,-21.71631 1.82765,-50.07477 l 16.61352,-17.23457 -4.608,-11.73082 -4.608,-11.73082 -29.78067,-2.02985 -29.78067,-2.02985 0.80253,-33.29047 0.80253,-33.29047 28.48749,-2.85715 28.48749,-2.85714 3.28334,-11.42857 3.28334,-11.42857 -18.1994,-19.00791 c -10.00967,-10.45436 -18.1994,-20.12738 -18.1994,-21.4956 0,-1.36824 10.28354,-12.71092 22.85231,-25.20598 l 22.85231,-22.71828 19.3964,19.24778 19.3964,19.24777 13.13154,-4.47437 13.13155,-4.47437 1.15055,-26.27381 c 0.63281,-14.45059 2.14472,-27.88095 3.3598,-29.84523 1.55902,-2.52031 11.82996,-3.57143 34.89776,-3.57143 l 32.68852,0 0,22.67114 c 0,30.00654 2.58714,36.16256 17.01434,40.48506 l 11.55808,3.46288 18.32364,-17.59525 c 10.078,-9.67739 19.89161,-17.59526 21.80802,-17.59526 1.9164,0 13.92055,10.23947 26.67586,22.75438 l 23.19149,22.75438 -19.88883,20.06797 -19.88886,20.06796 4.43392,12.17766 c 4.09622,11.25017 5.3934,12.32472 17.03171,14.10873 6.92877,1.06209 19.99063,2.02637 29.02634,2.14285 l 16.42857,0.21178 0,34.28572 0,34.28571 -21.07142,0 c -33.44695,0 -38.35323,1.70239 -42.23369,14.65427 l -3.32149,11.0861 19.02758,19.70916 c 10.4652,10.84003 19.0276,21.34799 19.0276,23.35102 0,4.76402 -40.74506,45.48517 -45.51189,45.48517 -2.01771,0 -12.90991,-8.96083 -24.20489,-19.91295 -20.44586,-19.82525 -20.58136,-19.89914 -30.76296,-16.77431 l -10.22664,3.13866 -2.50396,31.77428 L 409,716.6479 l -33.32487,0.80283 -33.32488,0.80282 -2.03213,-31.35391 z m 58.05936,-96.11759 c 41.48929,-17.33107 50.46415,-70.92953 16.82283,-100.467 -5.72605,-5.02754 -15.82082,-10.5977 -22.43283,-12.37813 -14.95988,-4.02828 -38.40229,-1.33506 -50.05351,5.75047 -22.67204,13.78769 -35.67939,49.00392 -26.7625,72.45711 3.99028,10.49523 19.70634,27.24005 31.3352,33.38637 12.72992,6.72828 36.58078,7.31236 51.09081,1.25118 z"
+ style="fill:#a0a0a0;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3077"
+ d="m 340.31239,686.8111 -2.0264,-31.2654 -10.35728,-4.13652 -10.35728,-4.13651 -22.11154,21.11617 c -12.16135,11.61391 -23.18839,21.1162 -24.50454,21.1162 -1.31614,0 -12.4099,-10.29334 -24.65281,-22.87409 -25.3507,-26.05026 -25.50888,-21.71631 1.82765,-50.07477 l 16.61352,-17.23457 -4.73383,-12.05114 c -4.33898,-11.04598 -5.59133,-12.05284 -15.01471,-12.07146 -5.65448,-0.0112 -18.95946,-0.83994 -29.5666,-1.84172 l -19.28571,-1.82142 0,-33.48294 0,-33.48293 29.21025,-2.53248 29.21025,-2.53248 3.27487,-11.42857 3.27486,-11.42857 -18.1994,-19.00791 c -10.00967,-10.45436 -18.1994,-20.12738 -18.1994,-21.4956 0,-1.36824 10.28354,-12.71092 22.85231,-25.20598 l 22.85231,-22.71828 19.3964,19.24778 19.3964,19.24777 13.13154,-4.47437 13.13155,-4.47437 1.15055,-26.27381 c 0.63281,-14.45059 2.14472,-27.88095 3.3598,-29.84523 1.55902,-2.52031 11.82996,-3.57143 34.89776,-3.57143 l 32.68852,0 0.2848,22.14286 c 0.406,31.56613 2.03422,35.66433 16.12267,40.58042 l 11.8983,4.15186 18.45696,-17.72329 c 10.15133,-9.7478 20.02494,-17.72328 21.94135,-17.72328 1.9164,0 13.92055,10.23947 26.67586,22.75438 l 23.19149,22.75438 -19.88883,20.06797 -19.88886,20.06796 4.43392,12.17766 c 4.09622,11.25017 5.3934,12.32472 17.03171,14.10873 6.92877,1.06209 19.99063,2.02637 29.02634,2.14285 l 16.42857,0.21178 0,34.28572 0,34.28571 -23.57142,0.0421 c -27.72475,0.0495 -36.66832,3.7013 -40.32949,16.46697 -2.54951,8.88964 -1.8394,10.14932 16.33206,28.97166 10.44857,10.82287 18.99743,21.31677 18.99743,23.3198 0,4.74805 -40.73712,45.48517 -45.48518,45.48517 -2.00302,0 -12.29835,-8.35715 -22.87852,-18.57143 -18.70259,-18.05583 -23.52162,-20.28709 -36.37094,-16.84 -5.69473,1.52771 -6.31799,4.13083 -8.1997,34.24668 l -2.03651,32.59332 -33.70233,0 -33.70233,0 -2.02639,-31.26537 z m 58.06509,-96.02905 c 31.3829,-13.10939 46.15865,-49.6639 32.35734,-80.05056 -16.96561,-37.35354 -70.38585,-46.62407 -98.95583,-17.17275 -24.62497,25.38461 -24.40007,61.43104 0.53077,85.07441 16.93513,16.06056 44.54805,21.13818 66.06772,12.1489 l 0,0 z"
+ style="fill:#ffffff;stroke:none" />
+ </g>
+ </g>
+</svg>
diff --git a/extensions/cpsection/Makefile.am b/extensions/cpsection/Makefile.am
index 2074d11..fef9e52 100644
--- a/extensions/cpsection/Makefile.am
+++ b/extensions/cpsection/Makefile.am
@@ -2,6 +2,7 @@ SUBDIRS = \
aboutme \
aboutcomputer \
accessibility \
+ configuration \
datetime \
frame \
keyboard \
diff --git a/extensions/cpsection/configuration/Makefile.am b/extensions/cpsection/configuration/Makefile.am
new file mode 100644
index 0000000..9f3718a
--- /dev/null
+++ b/extensions/cpsection/configuration/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pkgdatadir)/extensions/cpsection/configuration
+
+sugar_PYTHON = \
+ __init__.py \
+ model.py \
+ view.py
diff --git a/extensions/cpsection/configuration/__init__.py b/extensions/cpsection/configuration/__init__.py
new file mode 100644
index 0000000..dd61992
--- /dev/null
+++ b/extensions/cpsection/configuration/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2012 Simon Schampijer <erikos at sugarlabs.org>
+# Copyright (C) 2012 Ajay Garg <ajay at activitycentral.com>
+#
+# 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; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+
+CLASS = 'Configuration'
+ICON = 'module-configuration'
+TITLE = _('Configuration')
diff --git a/extensions/cpsection/configuration/model.py b/extensions/cpsection/configuration/model.py
new file mode 100644
index 0000000..4fd798d
--- /dev/null
+++ b/extensions/cpsection/configuration/model.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2012 Simon Schampijer <erikos at sugarlabs.org>
+# Copyright (C) 2012 Ajay Garg <ajay at activitycentral.com>
+#
+# 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; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+import logging
+
+_logger = logging.getLogger('ControlPanel - Configuration')
diff --git a/extensions/cpsection/configuration/view.py b/extensions/cpsection/configuration/view.py
new file mode 100644
index 0000000..8389fd3
--- /dev/null
+++ b/extensions/cpsection/configuration/view.py
@@ -0,0 +1,432 @@
+# Copyright (C) 2012 Simon Schampijer <erikos at sugarlabs.org>
+# Copyright (C) 2012 Ajay Garg <ajay at activitycentral.com>
+#
+# 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; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import gobject
+from gettext import gettext as _
+
+from sugar.graphics import style
+from sugar.graphics.alert import Alert
+from sugar.graphics.icon import Icon
+
+from jarabe.model import buddy
+from jarabe.model import friends
+from jarabe.model import neighborhood
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+owner_model = buddy.get_owner_instance()
+friends_model = friends.get_model()
+neighborhood_model = neighborhood.get_model()
+
+class LeftAlignedLabelWidget(gtk.HBox):
+
+ def __init__(self, label):
+ gtk.HBox.__init__(self)
+ label_widget = gtk.Label(label)
+ label_widget.set_line_wrap(True)
+ self.pack_start(label_widget, expand=False)
+ self.show_all()
+
+
+class CenterAlignedLabelWidget(gtk.VBox):
+
+ def __init__(self, label):
+ gtk.VBox.__init__(self)
+ label_widget = gtk.Label(label)
+ label_widget.set_line_wrap(True)
+ self.pack_start(label_widget, expand=False)
+ self.show_all()
+
+
+class BuddyWidget(gtk.HBox):
+
+ def __init__(self, buddy):
+ gtk.HBox.__init__(self)
+ self._buddy = buddy
+
+ self._check_button = gtk.CheckButton(label=self._buddy.nick)
+ self._check_button.set_active(False)
+ self.pack_start(self._check_button, expand=False)
+ self.show_all()
+
+ def _is_buddy_selected(self):
+ return self._check_button.get_active()
+
+ def _get_buddy_key(self):
+ return self._buddy.key
+
+ def _get_buddy_nick(self):
+ return self._buddy.nick
+
+ def _get_buddy_account(self):
+ if hasattr(self._buddy, 'account'):
+ return self._buddy.account
+ return None
+
+ def _get_buddy_contact_id(self):
+ if hasattr(self._buddy, 'contact_id'):
+ return self._buddy.contact_id
+ return None
+
+
+class AddRemoveWidget(gtk.VBox):
+
+ def __init__(self, label, group_detail, add_button_clicked_cb,
+ remove_button_clicked_cb, index):
+ gtk.VBox.__init__(self)
+ self.set_homogeneous(False)
+ self.set_spacing(10)
+
+ self._potential_new_group = False
+ self._group_name = label
+ self._group_detail = group_detail
+
+ self._primary_box = gtk.HBox()
+ self._primary_box.set_homogeneous(False)
+ self._primary_box.set_spacing(10)
+ self.pack_start(self._primary_box, expand=False)
+ self._primary_box.show_all()
+
+ self._index = index
+ self._add_button_added = False
+ self._remove_button_added = False
+
+ self._label = gtk.Entry()
+ self._label.set_text(label)
+
+ # Do not allow an already existing group name to be modified.
+ if len(label) > 0:
+ self._label.set_sensitive(False)
+ # Else, this is a potentially new group.
+ # Mark this is as new.
+ # However, this will ACTUALLY be a new group,
+ # only if it is given a name.
+ # That check will be done, after the user clicks the final save
+ # button.
+ else:
+ self._potential_new_group = True
+
+ self._primary_box.pack_start(self._label, expand=False)
+
+ if not self._potential_new_group:
+ self._details_button = gtk.Button(_('View Details'))
+ self._view_id = self._details_button.connect('clicked', self.__view_details_cb)
+ self._primary_box.pack_start(self._details_button,
+ expand=False)
+
+ add_icon = Icon(icon_name='list-add')
+ self._add_button = gtk.Button()
+ self._add_button.set_image(add_icon)
+ self._add_button.connect('clicked',
+ add_button_clicked_cb,
+ self)
+
+ remove_icon = Icon(icon_name='list-remove')
+ self._remove_button = gtk.Button()
+ self._remove_button.set_image(remove_icon)
+ self._remove_button.connect('clicked',
+ remove_button_clicked_cb,
+ self)
+
+ self.__add_add_button()
+ self.__add_remove_button()
+
+ self._details_table = gtk.VBox()
+ self._details_table.set_spacing(20)
+ self._details_table.show_all()
+ self.pack_start(self._details_table, expand=False)
+
+ if self._potential_new_group:
+ info_label = LeftAlignedLabelWidget(_('You may batch add'
+ ' from the following available online buddies. Any'
+ ' currently offline buddy may be added later from the'
+ ' neighborhood view, when it comes online.'))
+ self.pack_start(info_label, expand=False)
+ self._friends_list_box = gtk.VBox()
+ self._friends_list_box.set_homogeneous(True)
+ self._friends_list_box.set_spacing(20)
+ self.pack_start(self._friends_list_box, expand=False)
+ self._populate_friends_list()
+
+ self.pack_start(gtk.HSeparator())
+
+ self._primary_box.show_all()
+ self.show_all()
+
+ def _populate_friends_list(self):
+ buddies = neighborhood_model.get_buddies()
+ for buddy in buddies:
+
+ # Only make the buddy visible, if it has a valid key at
+ # this time.
+ if ((hasattr(buddy, 'key')) and (buddy.key is not None)):
+
+ # Do not show self :)
+ if buddy.key == owner_model.get_key():
+ continue
+
+ self._friends_list_box.pack_start(BuddyWidget(buddy))
+
+ self._friends_list_box.show_all()
+
+ def _get_buddy_widgets(self):
+ return self._friends_list_box.get_children()
+
+ def _get_index(self):
+ return self._index
+
+ def _set_index(self, value):
+ self._index = value
+
+ def _get_entry(self):
+ return self._label.get_text()
+
+ def __add_add_button(self):
+ self._primary_box.pack_start(self._add_button, expand=False)
+ self._add_button_added = True
+
+ def _remove_remove_button_if_not_already(self):
+ if self._remove_button_added:
+ self.__remove_remove_button()
+
+ def __remove_remove_button(self):
+ self._primary_box.remove(self._remove_button)
+ self._remove_button_added = False
+
+ def _add_remove_button_if_not_already(self):
+ if not self._remove_button_added:
+ self.__add_remove_button()
+
+ def __add_remove_button(self):
+ self._primary_box.pack_start(self._remove_button, expand=False)
+ self._remove_button_added = True
+
+ def __activate_view_id(self):
+ self._details_button.disconnect(self._hide_id)
+ self._view_id = self._details_button.connect('clicked',
+ self.__view_details_cb)
+
+ def __activate_hide_id(self):
+ self._details_button.disconnect(self._view_id)
+ self._hide_id = self._details_button.connect('clicked',
+ self.__hide_details_cb)
+
+ def __view_details_cb(self, widget):
+ if self._group_detail is None:
+ return
+
+ last_operation_value = friends_model._get_last_group_operation(self._group_name)
+
+ self._last_operation_box = gtk.VBox()
+ self._last_operation_box.pack_start(LeftAlignedLabelWidget(_('Last'
+ ' Operation On This Group :: ')), expand=False)
+ self._last_operation_box.pack_start(CenterAlignedLabelWidget(
+ last_operation_value), expand=False)
+ self._last_operation_box.show_all()
+
+ self._details_table.pack_start(self._last_operation_box,
+ expand=False)
+
+ self._container = gtk.Table()
+ self._details_table.pack_start(self._container, expand=False)
+
+ headings_list = [_('Nick'), _('Last Operation Status')]
+ for i in range(0, len(headings_list)):
+ self._container.attach(gtk.Label(headings_list[i]), i, i+1,
+ 0, 1)
+ for i in range(0, len(headings_list)):
+ self._container.attach(gtk.Label(''), i, i+1, 1, 2)
+
+ index = 2
+ friend_keys_of_group = \
+ friends_model._get_friend_keys_of_group(self._group_name)
+
+ for friend_key in friend_keys_of_group:
+ friend_model = friends_model._get_friend_by_key(friend_key)
+ label_widgets = []
+ nick = friend_model.get_nick()
+ label_widgets.append(CenterAlignedLabelWidget(nick))
+
+ last_operation_status = \
+ friends_model._get_last_operation_status_of_friend_in_group(self._group_name,
+ friend_key)
+ label_widgets.append(CenterAlignedLabelWidget(last_operation_status))
+
+ for i in range(0, len(label_widgets)):
+ self._container.attach(label_widgets[i], i, i+1, index,
+ index+1)
+ index = index + 1
+
+ self._container.show_all()
+
+ self._details_button.set_label(_('Hide Details'))
+ self.__activate_hide_id()
+
+ def __hide_details_cb(self, widget):
+ self._details_table.remove(self._last_operation_box)
+ self._details_table.remove(self._container)
+
+ self._details_button.set_label(_('View Details'))
+ self.__activate_view_id()
+
+
+class MultiWidget(gtk.VBox):
+
+ def __init__(self):
+ gtk.VBox.__init__(self)
+ self.set_spacing(10)
+
+ def _add_widget(self, label, metadata):
+ new_widget = AddRemoveWidget(label,
+ metadata,
+ self.__add_button_clicked_cb,
+ self.__remove_button_clicked_cb,
+ len(self.get_children()))
+ self.add(new_widget)
+ self.show_all()
+ self._update_remove_button_statuses()
+
+ def _add_blank_entry(self):
+ self._add_widget('', None)
+
+ def __add_button_clicked_cb(self, add_button,
+ add_button_container):
+ self._add_blank_entry()
+ self._update_remove_button_statuses()
+
+ def __remove_button_clicked_cb(self, remove_button,
+ remove_button_container):
+ # Remove group from the model.
+ group_name = remove_button_container._get_entry()
+ friends_model.remove_group(group_name)
+
+ # Remove group from the view.
+ self.remove(remove_button_container)
+
+ self._update_remove_button_statuses()
+
+ def _update_remove_button_statuses(self):
+ children = self.get_children()
+
+ # Now, if there is only one entry, remove-button
+ # should not be shown.
+ if len(children) == 1:
+ children[0]._remove_remove_button_if_not_already()
+
+ # Alternatively, if there are more than 1 entries,
+ # remove-button should be shown for all.
+ if len(children) > 1:
+ for child in children:
+ child._add_remove_button_if_not_already()
+
+ def set_groups(self, groups):
+ self._groups = groups
+
+ def _pre_save_operations(self):
+ for child in self.get_children():
+ if child._potential_new_group:
+ group_name = child._get_entry()
+ if len(group_name) > 0:
+ friends_model.add_group(group_name, False)
+
+ # Also, add all the selected buddies as friends.
+ buddy_widgets = child._get_buddy_widgets()
+ for widget in buddy_widgets:
+ if widget._is_buddy_selected():
+ # Add as friend.
+ friends_model.make_friend_by_parameters(
+ widget._get_buddy_key(),
+ widget._get_buddy_nick(),
+ widget._get_buddy_account(),
+ widget._get_buddy_contact_id())
+
+ # Add as friend in group.
+ friends_model.add_friend_to_group(widget._get_buddy_key(),
+ group_name,
+ False)
+
+ # Perform just one disk-write for groups.
+ friends_model.save_groups()
+
+
+class Configuration(SectionView):
+ def __init__(self, model, alerts=None):
+ SectionView.__init__(self)
+
+ self._model = model
+
+ self.set_border_width(style.DEFAULT_SPACING * 2)
+ self.set_spacing(style.DEFAULT_SPACING)
+ group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+ workspace = gtk.VBox()
+ workspace.show()
+
+ separator = gtk.HSeparator()
+ workspace.pack_start(separator, expand=False)
+
+ label_friend_groups = gtk.Label(_('Groups configuration'))
+ label_friend_groups.set_alignment(0, 0)
+ workspace.pack_start(label_friend_groups, expand=False)
+
+ box_friend_groups = gtk.VBox()
+ box_friend_groups.set_border_width(style.DEFAULT_SPACING * 2)
+ box_friend_groups.set_spacing(style.DEFAULT_SPACING)
+
+ self._widget_table = MultiWidget()
+ box_friend_groups.pack_start(self._widget_table, expand=False)
+
+ save_button = gtk.Button()
+ save_button.set_alignment(0, 0)
+ save_button.set_label('Save')
+ save_button.connect('clicked', self.__save_button_clicked_cb)
+ box_save_button = gtk.HBox()
+ box_save_button.set_homogeneous(False)
+ box_save_button.pack_start(save_button, expand=False)
+ box_save_button.show_all()
+
+ box_friend_groups.pack_start(box_save_button, expand=False)
+
+ box_friend_groups.show_all()
+ workspace.pack_start(box_friend_groups, expand=False)
+
+ scrolled = gtk.ScrolledWindow()
+ scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ scrolled.add_with_viewport(workspace)
+ scrolled.show()
+ self.add(scrolled)
+
+ workspace.show_all()
+ self.setup()
+
+ def setup(self):
+ groups = friends_model._get_groups()
+ groups.sort()
+
+ if len(groups) == 0:
+ self._widget_table._add_blank_entry()
+ else:
+ for group_name in groups:
+ group_detail = \
+ friends_model._get_group_by_key_name(group_name)
+ self._widget_table._add_widget(group_name, group_detail)
+
+ def __save_button_clicked_cb(self, save_button):
+ save_button.set_sensitive(False)
+ self._widget_table._pre_save_operations()
diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py
index 941b174..d2902be 100644
--- a/src/jarabe/frame/activitiestray.py
+++ b/src/jarabe/frame/activitiestray.py
@@ -354,7 +354,9 @@ class BaseTransferButton(ToolButton):
def remove(self):
frame = jarabe.frame.get_view()
frame.remove_notification(self.notif_icon)
- self.props.parent.remove(self)
+ if (self.props.parent is not None) and \
+ (self in self.props.parent.get_children()):
+ self.props.parent.remove(self)
def __notify_state_cb(self, file_transfer, pspec):
logging.debug('_update state: %r %r', file_transfer.props.state,
@@ -471,6 +473,13 @@ class OutgoingTransferButton(BaseTransferButton):
frame.add_notification(self.notif_icon,
gtk.CORNER_TOP_LEFT)
+ # TODO: figure out why this is necessary to do.
+ # if this step is not done, then invoking
+ # "__dismiss_clicked_cb()" WITHOUT clicking the "Dismiss"
+ # option (as in the case of auto-dismiss for bulk
+ # operations), DOES NOT WORK.
+ self.create_palette()
+
def create_palette(self):
palette = OutgoingTransferPalette(self.file_transfer)
palette.connect('dismiss-clicked', self.__dismiss_clicked_cb)
@@ -480,6 +489,14 @@ class OutgoingTransferButton(BaseTransferButton):
def __dismiss_clicked_cb(self, palette):
self.remove()
+ bulk_operation_details = \
+ self.file_transfer._get_bulk_operation_details()
+ if bulk_operation_details is not None:
+ group_name = bulk_operation_details._get_group_name()
+ friend_keys = bulk_operation_details._get_friend_keys()
+ counter = bulk_operation_details._get_counter()
+ proceed_cb = bulk_operation_details._get_proceed_cb()
+ proceed_cb(group_name, friend_keys, counter)
class BaseTransferPalette(Palette):
@@ -565,6 +582,7 @@ class IncomingTransferPalette(BaseTransferPalette):
def _update(self):
logging.debug('_update state: %r', self.file_transfer.props.state)
+
if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING:
menu_item = MenuItem(_('Accept'), icon_name='dialog-ok')
menu_item.connect('activate', self.__accept_activate_cb)
@@ -595,7 +613,6 @@ class IncomingTransferPalette(BaseTransferPalette):
elif self.file_transfer.props.state in \
[filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]:
-
for item in self.menu.get_children():
self.menu.remove(item)
@@ -619,7 +636,6 @@ class IncomingTransferPalette(BaseTransferPalette):
self.update_progress()
elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED:
-
for item in self.menu.get_children():
self.menu.remove(item)
@@ -629,8 +645,8 @@ class IncomingTransferPalette(BaseTransferPalette):
menu_item.show()
self.update_progress()
- elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED:
+ elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED:
for item in self.menu.get_children():
self.menu.remove(item)
@@ -684,11 +700,21 @@ class OutgoingTransferPalette(BaseTransferPalette):
self.progress_bar = None
self.progress_label = None
+ self._bulk_operation_details = \
+ file_transfer._get_bulk_operation_details()
self.file_transfer.connect('notify::state', self.__notify_state_cb)
nick = str(file_transfer.buddy.props.nick)
- label = glib.markup_escape_text(_('Transfer to %s') % (nick,))
+
+ label = None
+ if self._bulk_operation_details is None:
+ label = glib.markup_escape_text(_('Transfer to %s') % (nick,))
+ else:
+ counter = self._bulk_operation_details._get_counter()
+ total = self._bulk_operation_details._get_total()
+ label = glib.markup_escape_text(_('( %d / %d ) Transfer to %s') \
+ % (counter, total, nick,))
self.props.secondary_text = label
self._update()
@@ -699,8 +725,8 @@ class OutgoingTransferPalette(BaseTransferPalette):
def _update(self):
new_state = self.file_transfer.props.state
logging.debug('_update state: %r', new_state)
- if new_state == filetransfer.FT_STATE_PENDING:
+ if new_state == filetransfer.FT_STATE_PENDING:
menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel')
menu_item.connect('activate', self.__cancel_activate_cb)
self.menu.append(menu_item)
@@ -725,7 +751,6 @@ class OutgoingTransferPalette(BaseTransferPalette):
elif new_state in [filetransfer.FT_STATE_ACCEPTED,
filetransfer.FT_STATE_OPEN]:
-
for item in self.menu.get_children():
self.menu.remove(item)
@@ -750,7 +775,6 @@ class OutgoingTransferPalette(BaseTransferPalette):
elif new_state in [filetransfer.FT_STATE_COMPLETED,
filetransfer.FT_STATE_CANCELLED]:
-
for item in self.menu.get_children():
self.menu.remove(item)
@@ -758,9 +782,36 @@ class OutgoingTransferPalette(BaseTransferPalette):
menu_item.connect('activate', self.__dismiss_activate_cb)
self.menu.append(menu_item)
menu_item.show()
-
self.update_progress()
+ # Perform actions specific to bulk-operation.
+ if self._bulk_operation_details is not None:
+
+ # Update the peration status.
+ status = None
+ if new_state == filetransfer.FT_STATE_COMPLETED:
+ status = _('Success.')
+ elif new_state == filetransfer.FT_STATE_CANCELLED:
+ if self.file_transfer.reason_last_change == \
+ filetransfer.FT_REASON_REMOTE_STOPPED:
+ status = _('FAILURE:\tOperation Cancelled Remotely.')
+ elif self.file_transfer.reason_last_change == \
+ filetransfer.FT_REASON_LOCAL_STOPPED:
+ status = _('FAILURE:\tOperation Cancelled Locally.')
+
+ self._set_bulk_operation_status_if_applicable(status)
+
+ # "Dismiss' automatically for all, except the last of
+ # the bulk-operation.
+ counter = self._bulk_operation_details._get_counter()
+ total = self._bulk_operation_details._get_total()
+ if counter < total:
+ self.__dismiss_activate_cb(None)
+
+ def _set_bulk_operation_status_if_applicable(self, status):
+ if self._bulk_operation_details is not None:
+ self._bulk_operation_details._set_operation_status(status)
+
def __cancel_activate_cb(self, menu_item):
self.file_transfer.cancel()
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 8fc1e5d..e1d1e7d 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -37,6 +37,39 @@ from jarabe.model import mimeregistry
from jarabe.journal import misc
from jarabe.journal import model
+friends_model = friends.get_model()
+
+
+class BulkOperationDetails():
+
+ def __init__(self, group_name, friend, friend_keys, total, counter, proceed_cb):
+ self._group_name = group_name
+ self._friend = friend
+ self._friend_keys = friend_keys
+ self._counter = counter
+ self._total = total
+ self._proceed_cb = proceed_cb
+
+ def _get_group_name(self):
+ return self._group_name
+
+ def _get_friend_keys(self):
+ return self._friend_keys
+
+ def _get_counter(self):
+ return self._counter
+
+ def _get_total(self):
+ return self._total
+
+ def _get_proceed_cb(self):
+ return self._proceed_cb
+
+ def _set_operation_status(self, status):
+ friends_model._set_last_operation_status_of_friend_in_group(self._group_name,
+ self._friend.get_key(),
+ status)
+
class ObjectPalette(Palette):
@@ -117,6 +150,10 @@ class ObjectPalette(Palette):
friends_menu.connect('friend-selected', self.__friend_selected_cb)
menu_item.set_submenu(friends_menu)
+ groups_menu = GroupsMenu()
+ groups_menu.connect('group-selected', self.__group_selected_cb)
+ friends_menu._set_group_menu(groups_menu)
+
if detail == True:
menu_item = MenuItem(_('View Details'), 'go-right')
menu_item.connect('activate', self.__detail_activate_cb)
@@ -150,7 +187,8 @@ class ObjectPalette(Palette):
def __volume_error_cb(self, menu_item, message, severity):
self.emit('volume-error', message, severity)
- def __friend_selected_cb(self, menu_item, buddy):
+ def __friend_selected_cb(self, menu_item, buddy,
+ bulk_operation_details=None):
logging.debug('__friend_selected_cb')
file_name = model.get_file(self._metadata['uid'])
@@ -167,9 +205,51 @@ class ObjectPalette(Palette):
if not mime_type:
mime_type = mime.get_for_file(file_name)
-
filetransfer.start_transfer(buddy, file_name, title, description,
- mime_type)
+ mime_type, bulk_operation_details)
+
+ def __group_selected_cb(self, menu_item, group_name):
+ logging.debug('__group_selected_cb')
+ if group_name is not None:
+ friends_model._set_last_group_operation(group_name,
+ _('(TRANSFER) %s') % (self._metadata['title'],))
+ friends_model._set_last_operation_status_of_friends_in_group_with_common_status(
+ group_name, _('PENDING ...'))
+ friend_keys = \
+ friends_model._get_friend_keys_of_group(group_name)
+
+ self._proceed_with_next_friend(group_name, friend_keys, 0)
+
+ """
+ This is the (callback) function that needs to be called per friend.
+ Note that this function is a callback (and not a looped one), since
+ the "next" friend iteration begins, only when the current iteration
+ has finished - which is asynchronous.
+ """
+ def _proceed_with_next_friend(self, group_name, friend_keys, counter):
+ counter = counter + 1
+
+ if counter <= len(friend_keys):
+ friend_key = friend_keys[counter-1]
+ friend = friends_model._get_friend_by_key(friend_key)
+
+ bulk_operation_details = \
+ BulkOperationDetails(group_name,
+ friend,
+ friend_keys,
+ len(friend_keys),
+ counter,
+ self._proceed_with_next_friend)
+
+ # Only proceed if the friend is online.
+ # Else, set the failure-status, and move forward.
+ if friend.is_present():
+ self.__friend_selected_cb(None, friend, bulk_operation_details)
+ else:
+ bulk_operation_details._set_operation_status(_('FAILURE:\tFriend'
+ ' is offline.'))
+ self._proceed_with_next_friend(group_name, friend_keys,
+ counter)
class CopyMenu(gtk.Menu):
@@ -295,6 +375,41 @@ class ClipboardMenu(MenuItem):
self._temp_file_path = None
+class GroupsMenu(gtk.Menu):
+ __gtype_name__ = 'GroupsMenu'
+
+ __gsignals__ = {
+ 'group-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ if filetransfer.file_transfer_available():
+ for group in friends_model._get_groups():
+ menu_item = MenuItem(text_label=group,
+ icon_name='zoom-groups')
+ menu_item.connect('activate', self.__item_activate_cb,
+ group)
+ self.append(menu_item)
+ menu_item.show()
+
+ if not self.get_children():
+ menu_item = MenuItem(_('No groups present'))
+ menu_item.set_sensitive(False)
+ self.append(menu_item)
+ menu_item.show()
+ else:
+ menu_item = MenuItem(_('No valid connection found'))
+ menu_item.set_sensitive(False)
+ self.append(menu_item)
+ menu_item.show()
+
+ def __item_activate_cb(self, menu_item, group):
+ self.emit('group-selected', group)
+
+
class FriendsMenu(gtk.Menu):
__gtype_name__ = 'JournalFriendsMenu'
@@ -307,7 +422,6 @@ class FriendsMenu(gtk.Menu):
gobject.GObject.__init__(self)
if filetransfer.file_transfer_available():
- friends_model = friends.get_model()
for friend in friends_model:
if friend.is_present():
menu_item = MenuItem(text_label=friend.get_nick(),
@@ -332,6 +446,12 @@ class FriendsMenu(gtk.Menu):
def __item_activate_cb(self, menu_item, friend):
self.emit('friend-selected', friend)
+ def _set_group_menu(self, group_menu):
+ menu_item = MenuItem(_('Select a group, to send'))
+ menu_item.set_submenu(group_menu)
+ self.append(menu_item)
+ menu_item.show()
+
class StartWithMenu(gtk.Menu):
__gtype_name__ = 'JournalStartWithMenu'
diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py
index 8f17d7e..c6c6a4c 100644
--- a/src/jarabe/model/buddy.py
+++ b/src/jarabe/model/buddy.py
@@ -43,6 +43,7 @@ class BaseBuddyModel(gobject.GObject):
self._color = None
self._tags = None
self._current_activity = None
+ self._groups = None
gobject.GObject.__init__(self, **kwargs)
@@ -87,6 +88,16 @@ class BaseBuddyModel(gobject.GObject):
getter=get_current_activity,
setter=set_current_activity)
+ def get_groups(self):
+ return self._groups
+
+ def set_groups(self, groups):
+ self._groups = groups
+
+ groups = gobject.property(type=object,
+ getter=get_groups,
+ setter=set_groups)
+
def is_owner(self):
raise NotImplementedError
@@ -179,6 +190,7 @@ class BuddyModel(BaseBuddyModel):
self._account = None
self._contact_id = None
self._handle = None
+ self._groups_list = []
BaseBuddyModel.__init__(self, **kwargs)
diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py
index 447a74a..3de8c20 100644
--- a/src/jarabe/model/filetransfer.py
+++ b/src/jarabe/model/filetransfer.py
@@ -124,6 +124,7 @@ class BaseFileTransfer(gobject.GObject):
self.mime_type = None
self.initial_offset = 0
self.reason_last_change = FT_REASON_NONE
+ self._bulk_operation_details = None
def set_channel(self, channel):
self.channel = channel
@@ -156,6 +157,12 @@ class BaseFileTransfer(gobject.GObject):
def _get_transferred_bytes(self):
return self._transferred_bytes
+ def _get_bulk_operation_details(self):
+ return self._bulk_operation_details
+
+ def _set_bulk_operation_details(self, bulk_operation_details):
+ self._bulk_operation_details = bulk_operation_details
+
transferred_bytes = gobject.property(type=int, default=0,
getter=_get_transferred_bytes, setter=_set_transferred_bytes)
@@ -226,7 +233,8 @@ class IncomingFileTransfer(BaseFileTransfer):
class OutgoingFileTransfer(BaseFileTransfer):
- def __init__(self, buddy, file_name, title, description, mime_type):
+ def __init__(self, buddy, file_name, title, description, mime_type,
+ bulk_operation_details):
presence_service = presenceservice.get_instance()
name, path = presence_service.get_preferred_connection()
@@ -241,6 +249,7 @@ class OutgoingFileTransfer(BaseFileTransfer):
self._socket = None
self._splicer = None
self._output_stream = None
+ self._set_bulk_operation_details(bulk_operation_details)
self.buddy = buddy
self.title = title
@@ -324,9 +333,12 @@ def init():
_monitor_connection(connection)
-def start_transfer(buddy, file_name, title, description, mime_type):
+def start_transfer(buddy, file_name, title, description, mime_type,
+ bulk_operation_details):
outgoing_file_transfer = OutgoingFileTransfer(buddy, file_name, title,
- description, mime_type)
+ description,
+ mime_type,
+ bulk_operation_details)
new_file_transfer.send(None, file_transfer=outgoing_file_transfer)
diff --git a/src/jarabe/model/friends.py b/src/jarabe/model/friends.py
index 7605af1..448a36e 100644
--- a/src/jarabe/model/friends.py
+++ b/src/jarabe/model/friends.py
@@ -14,6 +14,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+from gettext import gettext as _
+
import os
import logging
from ConfigParser import ConfigParser
@@ -22,6 +24,7 @@ import gobject
import dbus
from sugar import env
+from sugar.util import unique_id
from sugar.graphics.xocolor import XoColor
from jarabe.model.buddy import BuddyModel
@@ -36,11 +39,11 @@ class FriendBuddyModel(BuddyModel):
_NOT_PRESENT_COLOR = '#D5D5D5,#FFFFFF'
- def __init__(self, nick, key, account=None, contact_id=None):
+ def __init__(self, nick, key, account=None, contact_id=None, groups=[]):
self._online_buddy = None
BuddyModel.__init__(self, nick=nick, key=key, account=account,
- contact_id=contact_id)
+ contact_id=contact_id, groups=groups)
neighborhood_model = neighborhood.get_model()
neighborhood_model.connect('buddy-added', self.__buddy_added_cb)
@@ -110,35 +113,239 @@ class Friends(gobject.GObject):
def __init__(self):
gobject.GObject.__init__(self)
-
- self._friends = {}
self._path = os.path.join(env.get_profile_path(), 'friends')
+ self._groups_path = os.path.join(env.get_profile_path(), 'groups')
+ self.reinit()
+ def reinit(self):
+ self._friends = {}
+ self._groups = {}
self.load()
def has_buddy(self, buddy):
- return buddy.get_key() in self._friends
+ return self.check_buddy_existence_by_key(buddy.get_key())
+
+ """
+ Ideally, this should be the only publically exposed API, since
+ only the buddy_key is the deciding factor of the existence of
+ a friend. There can never be two friends of the same key.
+ """
+ def check_buddy_existence_by_key(self, buddy_key):
+ return buddy_key in self._friends
def add_friend(self, buddy_info):
self._friends[buddy_info.get_key()] = buddy_info
self.emit('friend-added', buddy_info)
- def make_friend(self, buddy):
- if not self.has_buddy(buddy):
- buddy = FriendBuddyModel(key=buddy.key, nick=buddy.nick,
- account=buddy.account,
- contact_id=buddy.contact_id)
+ def add_friend_to_group(self, buddy_key, group_name,
+ save_group=True):
+ if not self.__group_exists(group_name):
+ return _('Cannot add to group !! Create this group first !!')
+
+ if self.__friend_exists_in_group(group_name, buddy_key):
+ return _('Friend already exists in this group')
+
+ # If we reach here, it is safe to add this buddy :)
+ self._groups[group_name]['friends'][buddy_key] = {}
+
+ # Also, update the association from the buddy side, and save.
+ groups = self._get_groups_of_a_friend(buddy_key)
+ if group_name not in groups:
+ groups.append(group_name)
+ groups.sort()
+ self.save()
+
+ if save_group:
+ self.save_groups()
+
+ def remove_friend_group_assoc_using_friend_key(self, group_name,
+ friend_key, save_friend=True, save_group=True):
+ if not self.__group_exists(group_name):
+ return _('No such group exists !!')
+
+ # Remove association from friend.
+ if group_name in self._friends[friend_key].get_groups():
+ self._friends[friend_key].get_groups().remove(group_name)
+
+ # Remove association from group.
+ if friend_key in self._groups[group_name]['friends'].keys():
+ del self._groups[group_name]['friends'][friend_key]
+
+ if save_friend:
+ self.save()
+ if save_group:
+ self.save_groups()
+
+ def _get_groups_of_a_friend(self, friend_key):
+ if friend_key not in self._friends.keys():
+ return []
+
+ return self._friends[friend_key].get_groups()
+
+ def add_group(self, group_name, save_group=True):
+ if self.__group_exists(group_name):
+ return _('A group with the same name already exists !!'
+ 'Choose a different group-name.')
+
+ # If we reach here, it is safe to add a group of this name.
+ self._groups[group_name] = {}
+ self._groups[group_name]['friends'] = {}
+ self._groups[group_name]['last-operation'] = 'NONE.'
+ if save_group:
+ self.save_groups()
+
+ def remove_group(self, group_name, save_friend=True,
+ save_group=True):
+ if not self.__group_exists(group_name):
+ return _('No group exists of this name !!')
+
+ # Remove this group, from all the friend-associations.
+ for friend_key in self._friends.keys():
+ friend_groups_list = self._friends[friend_key].get_groups()
+ if group_name in friend_groups_list:
+ friend_groups_list.remove(group_name)
+ if save_friend:
+ self.save()
+
+ # Remove the group itself.
+ del self._groups[group_name]
+ if save_group:
+ self.save_groups()
+
+ def __group_exists(self, group_name):
+ if group_name in self._groups.keys():
+ return True
+ return False
+
+ def __friend_exists_in_group(self, group_name, friend_key):
+ if friend_key in \
+ self._groups[group_name]['friends'].keys():
+ return True
+ return False
+
+ """
+ This method is useful in the case, when the buddy is to be added as
+ a friend in deferred sense. For example, the buddies list may first be
+ fetched by virtue of their being online, but may go offline by the
+ time they are about to be saved. So, in that case, we retrieve the
+ parameters that are required for saving in the early stages itself.
+ """
+ def make_friend_by_parameters(self, buddy_key, buddy_nick, buddy_account, buddy_contact_id):
+ if not self.check_buddy_existence_by_key(buddy_key):
+ buddy = FriendBuddyModel(key=buddy_key,
+ nick=buddy_nick,
+ account=buddy_account,
+ contact_id=buddy_contact_id)
self.add_friend(buddy)
self.save()
+ def make_friend(self, buddy):
+ self.make_friend_by_parameters(buddy.key, buddy.nick,
+ buddy.account, buddy.contact_id)
+
+ def remove_groups_of_friend(self, buddy_info, save_friend=True,
+ save_group=True):
+ groups = self._get_groups_of_a_friend(buddy_info.get_key())
+ for group in groups:
+ self.remove_friend_group_assoc_using_friend_key(group,
+ buddy_info.get_key(), False, False)
+
+ if save_friend:
+ self.save()
+ if save_group:
+ self.save_groups()
+
def remove(self, buddy_info):
+ # First remove its association from all its groups
+ self.remove_groups_of_friend(buddy_info, False, True)
+
+ # Now, remove the friend-entity itself.
del self._friends[buddy_info.get_key()]
self.save()
+
self.emit('friend-removed', buddy_info.get_key())
def __iter__(self):
return self._friends.values().__iter__()
+ def _get_friend_by_key(self, key):
+ if not (key in self._friends.keys()):
+ return _('No friend found !!')
+ return self._friends[key]
+
+ def _get_friend_keys_of_group(self, group_name):
+ if not self.__group_exists(group_name):
+ return []
+
+ return self._groups[group_name]['friends'].keys()
+
+ def _get_groups(self):
+ # Sascha's wonderful feedback: always export only the thing
+ # required. Here, we required only the group-names; so export
+ # just the keys
+ return self._groups.keys()
+
+ def _set_groups(self, groups):
+ self._groups = groups
+
+ def _get_group_by_key_name(self, group_name):
+ if not self.__group_exists(group_name):
+ return None
+ return self._groups[group_name]
+
+ def _get_last_group_operation(self, group_name):
+ if not self.__group_exists(group_name):
+ return _('No group of this name exists !!')
+ return self._groups[group_name]['last-operation']
+
+ def _set_last_group_operation(self, group_name, operation):
+ if not self.__group_exists(group_name):
+ return _('No group exists.')
+ self._groups[group_name]['last-operation'] = operation
+ self.save_groups
+
+ def _get_last_operation_status_of_friend_in_group(self, group_name,
+ friend_key):
+ if not self.__group_exists(group_name):
+ return _('No group exists.')
+
+ if not self.__friend_exists_in_group(group_name, friend_key):
+ return _('Friend does not exist in group')
+
+ if 'last-operation-status' in \
+ self._groups[group_name]['friends'][friend_key].keys():
+ return self._groups[group_name]['friends'][friend_key]['last-operation-status']
+
+ def _set_last_operation_status_of_friend_in_group(self, group_name,
+ friend_key, status,
+ save_group=True):
+ if not self.__group_exists(group_name):
+ return _('No group exists.')
+
+ if not self.__friend_exists_in_group(group_name, friend_key):
+ return _('Friend does not exist in group')
+
+ self._groups[group_name]['friends'][friend_key]['last-operation-status'] = \
+ status
+
+ if save_group:
+ self.save_groups()
+
+ def _set_last_operation_status_of_friends_in_group_with_common_status(
+ self, group_name, status):
+ if not self.__group_exists(group_name):
+ return _('No group exists.')
+
+ friend_keys = self._get_friend_keys_of_group(group_name)
+ for friend_key in friend_keys:
+ self._set_last_operation_status_of_friend_in_group(group_name,
+ friend_key,
+ status,
+ False)
+
+ # Save the groups in one go.
+ self.save_groups()
+
def load(self):
cp = ConfigParser()
@@ -149,11 +356,36 @@ class Friends(gobject.GObject):
# HACK: don't screw up on old friends files
if len(key) < 20:
continue
- buddy = FriendBuddyModel(key=key, nick=cp.get(key, 'nick'))
+
+ # Check for the existence of 'groups' option (for
+ # backwards compatability)
+ groups = []
+ if cp.has_option(key, 'groups'):
+ groups = eval(cp.get(key, 'groups'))
+
+ buddy = FriendBuddyModel(key=key, nick=cp.get(key,
+ 'nick'), groups=groups)
self.add_friend(buddy)
except Exception:
logging.exception('Error parsing friends file')
+ self.__load_groups()
+
+ def __load_groups(self):
+ cp = ConfigParser()
+
+ try:
+ success = cp.read([self._groups_path])
+ if success:
+ for group_name in cp.sections():
+ self._groups[group_name] = {}
+ self._groups[group_name]['friends'] = \
+ eval(cp.get(group_name, 'friends'))
+ self._groups[group_name]['last-operation'] = \
+ cp.get(group_name, 'last-operation')
+ except:
+ logging.exception('Error while loading config')
+
def save(self):
cp = ConfigParser()
@@ -161,11 +393,24 @@ class Friends(gobject.GObject):
section = friend.get_key()
cp.add_section(section)
cp.set(section, 'nick', friend.get_nick())
+ cp.set(section, 'groups', friend.get_groups())
fileobject = open(self._path, 'w')
cp.write(fileobject)
fileobject.close()
+ def save_groups(self):
+ cp = ConfigParser()
+
+ for group in self._groups.keys():
+ cp.add_section(group)
+ cp.set(group, 'friends', self._groups[group]['friends'])
+ cp.set(group, 'last-operation',
+ self._groups[group]['last-operation'])
+
+ fileobject = open(self._groups_path, 'w')
+ cp.write(fileobject)
+ fileobject.close()
def get_model():
global _model
diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py
index de5a772..544faeb 100644
--- a/src/jarabe/view/buddymenu.py
+++ b/src/jarabe/view/buddymenu.py
@@ -22,6 +22,7 @@ import gtk
import gconf
import glib
import dbus
+import gobject
from sugar.graphics.palette import Palette
from sugar.graphics.menuitem import MenuItem
@@ -33,6 +34,34 @@ from jarabe.model.session import get_session_manager
from jarabe.controlpanel.gui import ControlPanel
import jarabe.desktop.homewindow
+friends_model = friends.get_model()
+
+class GroupsMenu(gtk.Menu):
+ __gtype_name__ = 'NeighbourhoodGroupsMenu'
+
+ __gsignals__ = {
+ 'group-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ }
+
+ def __init__(self, groups, no_groups_label):
+ gobject.GObject.__init__(self)
+
+ for group in groups:
+ menu_item = MenuItem(text_label=group, icon_name='zoom-groups')
+ menu_item.connect('activate', self.__item_activate_cb, group)
+ self.append(menu_item)
+ menu_item.show()
+
+ if not self.get_children():
+ menu_item = MenuItem(no_groups_label)
+ menu_item.set_sensitive(False)
+ self.append(menu_item)
+ menu_item.show()
+
+ def __item_activate_cb(self, menu_item, group):
+ self.emit('group-selected', group)
+
class BuddyMenu(Palette):
def __init__(self, buddy):
@@ -64,19 +93,57 @@ class BuddyMenu(Palette):
def _add_buddy_items(self):
if friends.get_model().has_buddy(self._buddy):
- menu_item = MenuItem(_('Remove friend'), 'list-remove')
+ menu_item = MenuItem(_('Remove friend, and its association'
+ ' from all groups'), 'list-remove')
menu_item.connect('activate', self._remove_friend_cb)
+ self.menu.append(menu_item)
else:
- menu_item = MenuItem(_('Make friend'), 'list-add')
+ menu_item = MenuItem(_('Make friend (without associating'
+ ' to any group)'), 'list-add')
menu_item.connect('activate', self._make_friend_cb)
-
- self.menu.append(menu_item)
- menu_item.show()
+ self.menu.append(menu_item)
+
+ menu_item_2 = MenuItem(_('Make friend (if not already), and add to group'),
+ 'list-add')
+
+ all_groups = friends_model._get_groups()
+ groups_of_friend = \
+ friends_model._get_groups_of_a_friend(self._buddy.get_key())
+
+ # Make a local copy of groups, of which the friend is not
+ # associated with.
+ groups_not_of_friend = []
+ for group in all_groups:
+ groups_not_of_friend.append(group)
+ for group in groups_of_friend:
+ groups_not_of_friend.remove(group)
+
+ groups_of_friend.sort()
+ groups_not_of_friend.sort()
+
+ add_groups_menu = GroupsMenu(groups_not_of_friend, _('No groups to'
+ ' associate'))
+ add_groups_menu.connect('group-selected',
+ self.__make_friend_and_add_to_group)
+ self.menu.append(menu_item_2)
+ menu_item_2.set_submenu(add_groups_menu)
+ add_groups_menu.show()
+
+ menu_item_3 = MenuItem(_('Remove friend from group'), 'list-remove')
+ remove_groups_menu = GroupsMenu(groups_of_friend, _('No groups to'
+ ' disassociate'))
+ remove_groups_menu.connect('group-selected',
+ self.__remove_friend_from_group)
+ self.menu.append(menu_item_3)
+ menu_item_3.set_submenu(remove_groups_menu)
+ remove_groups_menu.show()
self._invite_menu = MenuItem('')
self._invite_menu.connect('activate', self._invite_friend_cb)
self.menu.append(self._invite_menu)
+ self.menu.show_all()
+
home_model = shell.get_model()
self._active_activity_changed_hid = home_model.connect(
'active-activity-changed', self._cur_activity_changed_cb)
@@ -154,7 +221,7 @@ class BuddyMenu(Palette):
def __buddy_notify_nick_cb(self, buddy, pspec):
self.set_primary_text(glib.markup_escape_text(buddy.props.nick))
- def _make_friend_cb(self, menuitem):
+ def _make_friend_cb(self, menuitem=None):
friends.get_model().make_friend(self._buddy)
def _remove_friend_cb(self, menuitem):
@@ -178,3 +245,11 @@ class BuddyMenu(Palette):
raise
else:
logging.error('Invite failed, activity service not ')
+
+ def __make_friend_and_add_to_group(self, menu_item, group):
+ self._make_friend_cb()
+ friends_model.add_friend_to_group(self._buddy.get_key(), group)
+
+ def __remove_friend_from_group(self, menu_item, group):
+ friends_model.remove_friend_group_assoc_using_friend_key(group,
+ self._buddy.get_key())
--
1.7.4.4
More information about the Sugar-devel
mailing list