diff options
author | Ajay Garg <ajay@activitycentral.com> | 2012-01-29 08:50:39 (GMT) |
---|---|---|
committer | Anish Mangal <anish@activitycentral.com> | 2012-04-27 10:02:36 (GMT) |
commit | 22f5218aaecc5f9caa94726b619bfcfbc38c544a (patch) | |
tree | e9cba461c6e78be49cde5402680a253e23127c44 | |
parent | 51caf0a7c3c045baee5a512ee5a7d11491b91c67 (diff) |
uy#1229: Journal-Entry transfer from 1-to-N users.
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | data/icons/module-configuration.svg | 190 | ||||
-rw-r--r-- | extensions/cpsection/Makefile.am | 1 | ||||
-rw-r--r-- | extensions/cpsection/configuration/Makefile.am | 6 | ||||
-rw-r--r-- | extensions/cpsection/configuration/__init__.py | 22 | ||||
-rw-r--r-- | extensions/cpsection/configuration/model.py | 21 | ||||
-rw-r--r-- | extensions/cpsection/configuration/view.py | 432 | ||||
-rw-r--r-- | src/jarabe/frame/activitiestray.py | 69 | ||||
-rw-r--r-- | src/jarabe/journal/palettes.py | 128 | ||||
-rw-r--r-- | src/jarabe/model/buddy.py | 12 | ||||
-rw-r--r-- | src/jarabe/model/filetransfer.py | 18 | ||||
-rw-r--r-- | src/jarabe/model/friends.py | 267 | ||||
-rw-r--r-- | src/jarabe/view/buddymenu.py | 87 |
13 files changed, 1221 insertions, 33 deletions
diff --git a/configure.ac b/configure.ac index 8e6d871..c028d14 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/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 1cbf8ea..7324431 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@sugarlabs.org> +# Copyright (C) 2012 Ajay Garg <ajay@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@sugarlabs.org> +# Copyright (C) 2012 Ajay Garg <ajay@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@sugarlabs.org> +# Copyright (C) 2012 Ajay Garg <ajay@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()) |