Attachments you submit will be routed for moderation. If you have an account, please log in first.

Ticket #3315: 0001-sl-3315.patch

File 0001-sl-3315.patch, 68.8 KB (added by ajay_garg, 16 months ago)
  • configure.ac

    From 7292a106212489b2573afecddf6a0e94c6963f22 Mon Sep 17 00:00:00 2001
    From: Ajay Garg <ajay@activitycentral.com>
    Date: Sun, 29 Jan 2012 14:20:39 +0530
    Subject: [PATCH] sl#3315: Journal-Entry transfer from 1-to-N users.
    Organization: Sugar Labs Foundation
    Signed-off-by: Ajay Garg <ajay@activitycentral.com>
    
    ---
    
    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 b  
    5252extensions/cpsection/aboutcomputer/Makefile 
    5353extensions/cpsection/accessibility/Makefile 
    5454extensions/cpsection/aboutme/Makefile 
     55extensions/cpsection/configuration/Makefile 
    5556extensions/cpsection/datetime/Makefile 
    5657extensions/cpsection/frame/Makefile 
    5758extensions/cpsection/keyboard/Makefile 
  • data/icons/Makefile.am

    diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am
    index 8e01626..60f735a 100644
    a b  
    44        module-about_me.svg             \ 
    55        module-about_my_computer.svg    \ 
    66        module-accessibility.svg        \ 
     7        module-configuration.svg        \ 
    78        module-date_and_time.svg        \ 
    89        module-frame.svg                \ 
    910        module-keyboard.svg             \ 
  • (a) /dev/null vs. (b) b/data/icons/module-configuration.svg

    diff --git a/data/icons/module-configuration.svg b/data/icons/module-configuration.svg
    new file mode 100644
    index 0000000..16ca355
    a b  
     1<?xml version="1.0" encoding="UTF-8" standalone="no"?> 
     2<!-- Created with Inkscape (http://www.inkscape.org/) --> 
     3 
     4<svg 
     5   xmlns:dc="http://purl.org/dc/elements/1.1/" 
     6   xmlns:cc="http://creativecommons.org/ns#" 
     7   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
     8   xmlns:svg="http://www.w3.org/2000/svg" 
     9   xmlns="http://www.w3.org/2000/svg" 
     10   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 
     11   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 
     12   width="744.09448819" 
     13   height="1052.3622047" 
     14   id="svg2" 
     15   version="1.1" 
     16   inkscape:version="0.48.1 r9760" 
     17   sodipodi:docname="module-configuration.svg"> 
     18  <defs 
     19     id="defs4"> 
     20    <inkscape:perspective 
     21       sodipodi:type="inkscape:persp3d" 
     22       inkscape:vp_x="0 : 526.18109 : 1" 
     23       inkscape:vp_y="0 : 1000 : 0" 
     24       inkscape:vp_z="744.09448 : 526.18109 : 1" 
     25       inkscape:persp3d-origin="372.04724 : 350.78739 : 1" 
     26       id="perspective6637" /> 
     27    <inkscape:perspective 
     28       id="perspective6615" 
     29       inkscape:persp3d-origin="0.5 : 0.33333333 : 1" 
     30       inkscape:vp_z="1 : 0.5 : 1" 
     31       inkscape:vp_y="0 : 1000 : 0" 
     32       inkscape:vp_x="0 : 0.5 : 1" 
     33       sodipodi:type="inkscape:persp3d" /> 
     34  </defs> 
     35  <sodipodi:namedview 
     36     id="base" 
     37     pagecolor="#ffffff" 
     38     bordercolor="#666666" 
     39     borderopacity="1.0" 
     40     inkscape:pageopacity="0.0" 
     41     inkscape:pageshadow="2" 
     42     inkscape:zoom="0.35" 
     43     inkscape:cx="375" 
     44     inkscape:cy="514.28571" 
     45     inkscape:document-units="px" 
     46     inkscape:current-layer="layer1" 
     47     showgrid="false" 
     48     inkscape:window-width="1366" 
     49     inkscape:window-height="693" 
     50     inkscape:window-x="0" 
     51     inkscape:window-y="25" 
     52     inkscape:window-maximized="1" /> 
     53  <metadata 
     54     id="metadata7"> 
     55    <rdf:RDF> 
     56      <cc:Work 
     57         rdf:about=""> 
     58        <dc:format>image/svg+xml</dc:format> 
     59        <dc:type 
     60           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 
     61        <dc:title></dc:title> 
     62      </cc:Work> 
     63    </rdf:RDF> 
     64  </metadata> 
     65  <g 
     66     inkscape:label="Layer 1" 
     67     inkscape:groupmode="layer" 
     68     id="layer1"> 
     69    <g 
     70       id="layer1-3" 
     71       inkscape:label="Layer 1" 
     72       transform="matrix(2.0112611,0,0,2.8271726,-382.79436,-991.72999)"> 
     73      <g 
     74         inkscape:label="Layer 1" 
     75         id="layer1-1" 
     76         transform="translate(170.0671,-314.28571)"> 
     77        <g 
     78           transform="matrix(9.8137136,0,0,9.8137136,-2250.1262,598.08659)" 
     79           id="g6596"> 
     80          <g 
     81             id="g7197"> 
     82            <path 
     83               sodipodi:type="arc" 
     84               style="fill:none;stroke:#00000f;stroke-width:9.03419971;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" 
     85               id="path5989" 
     86               sodipodi:cx="307.3714" 
     87               sodipodi:cy="24.611456" 
     88               sodipodi:rx="12.380237" 
     89               sodipodi:ry="11.953332" 
     90               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" 
     91               transform="matrix(0.77926228,0,0,0.79380856,10.613376,6.1120011)" /> 
     92            <rect 
     93               style="fill:#00000f;fill-opacity:1;stroke:none" 
     94               id="rect6505" 
     95               width="7.9117804" 
     96               height="9.2506971" 
     97               x="246.11057" 
     98               y="7.1083627" 
     99               ry="0.82999998" 
     100               rx="0.82999998" /> 
     101            <rect 
     102               style="fill:#00000f;fill-opacity:1;stroke:none" 
     103               id="rect6505-9" 
     104               width="7.9117804" 
     105               height="9.2506971" 
     106               x="246.21878" 
     107               y="35.461403" 
     108               rx="0.82999998" 
     109               ry="0.82999998" /> 
     110            <rect 
     111               style="fill:#00000f;fill-opacity:1;stroke:none" 
     112               id="rect6505-7" 
     113               width="7.9117804" 
     114               height="9.2506971" 
     115               x="-29.96331" 
     116               y="231.45294" 
     117               transform="matrix(0,-1,1,0,0,0)" 
     118               ry="0.82999998" 
     119               rx="0.82999998" /> 
     120            <rect 
     121               style="fill:#00000f;fill-opacity:1;stroke:none" 
     122               id="rect6505-6" 
     123               width="7.9117804" 
     124               height="9.2506971" 
     125               x="-29.848055" 
     126               y="259.62869" 
     127               transform="matrix(0,-1,1,0,0,0)" 
     128               ry="0.82999998" 
     129               rx="0.82999998" /> 
     130            <rect 
     131               style="fill:#00000f;fill-opacity:1;stroke:none" 
     132               id="rect6505-76" 
     133               width="7.9117804" 
     134               height="9.2506971" 
     135               x="154.41437" 
     136               y="176.47606" 
     137               transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" 
     138               ry="0.82999998" 
     139               rx="0.82999998" /> 
     140            <rect 
     141               style="fill:#00000f;fill-opacity:1;stroke:none" 
     142               id="rect6505-76-7" 
     143               width="7.9117804" 
     144               height="9.2506971" 
     145               x="-199.0881" 
     146               y="-177.31622" 
     147               transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,0,0)" 
     148               ry="0.82999998" 
     149               rx="0.82999998" /> 
     150            <rect 
     151               style="fill:#00000f;fill-opacity:1;stroke:none" 
     152               id="rect6505-76-5" 
     153               width="7.9117804" 
     154               height="9.2506971" 
     155               x="154.6358" 
     156               y="204.75911" 
     157               transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" 
     158               ry="0.82999998" 
     159               rx="0.82999998" /> 
     160            <rect 
     161               style="fill:#00000f;fill-opacity:1;stroke:none" 
     162               id="rect6505-76-0" 
     163               width="7.9117804" 
     164               height="9.2506971" 
     165               x="-199.24612" 
     166               y="-148.95982" 
     167               transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,0,0)" 
     168               ry="0.82999998" 
     169               rx="0.82999998" /> 
     170          </g> 
     171        </g> 
     172      </g> 
     173      <path 
     174         inkscape:connector-curvature="0" 
     175         id="path3073" 
     176         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" 
     177         style="fill:#a0a0a0;stroke:none" /> 
     178      <path 
     179         inkscape:connector-curvature="0" 
     180         id="path3075" 
     181         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" 
     182         style="fill:#a0a0a0;stroke:none" /> 
     183      <path 
     184         inkscape:connector-curvature="0" 
     185         id="path3077" 
     186         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" 
     187         style="fill:#ffffff;stroke:none" /> 
     188    </g> 
     189  </g> 
     190</svg> 
  • extensions/cpsection/Makefile.am

    diff --git a/extensions/cpsection/Makefile.am b/extensions/cpsection/Makefile.am
    index 2074d11..fef9e52 100644
    a b  
    22        aboutme \ 
    33        aboutcomputer \ 
    44        accessibility \ 
     5        configuration \ 
    56        datetime \ 
    67        frame \ 
    78        keyboard \ 
  • (a) /dev/null vs. (b) b/extensions/cpsection/configuration/Makefile.am

    diff --git a/extensions/cpsection/configuration/Makefile.am b/extensions/cpsection/configuration/Makefile.am
    new file mode 100644
    index 0000000..9f3718a
    a b  
     1sugardir = $(pkgdatadir)/extensions/cpsection/configuration 
     2 
     3sugar_PYTHON =          \ 
     4        __init__.py     \ 
     5        model.py        \ 
     6        view.py          
  • (a) /dev/null vs. (b) b/extensions/cpsection/configuration/__init__.py

    diff --git a/extensions/cpsection/configuration/__init__.py b/extensions/cpsection/configuration/__init__.py
    new file mode 100644
    index 0000000..dd61992
    a b  
     1# Copyright (C) 2012 Simon Schampijer <erikos@sugarlabs.org> 
     2# Copyright (C) 2012 Ajay Garg        <ajay@activitycentral.com> 
     3# 
     4# This program is free software; you can redistribute it and/or modify 
     5# it under the terms of the GNU General Public License as published by 
     6# the Free Software Foundation; either version 2 of the License, or 
     7# (at your option) any later version. 
     8# 
     9# This program is distributed in the hope that it will be useful, 
     10# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
     12# GNU General Public License for more details. 
     13# 
     14# You should have received a copy of the GNU General Public License 
     15# along with this program; if not, write to the Free Software 
     16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
     17 
     18from gettext import gettext as _ 
     19 
     20CLASS = 'Configuration' 
     21ICON = 'module-configuration' 
     22TITLE = _('Configuration') 
  • (a) /dev/null vs. (b) b/extensions/cpsection/configuration/model.py

    diff --git a/extensions/cpsection/configuration/model.py b/extensions/cpsection/configuration/model.py
    new file mode 100644
    index 0000000..4fd798d
    a b  
     1# Copyright (C) 2012 Simon Schampijer <erikos@sugarlabs.org> 
     2# Copyright (C) 2012 Ajay Garg        <ajay@activitycentral.com> 
     3# 
     4# This program is free software; you can redistribute it and/or modify 
     5# it under the terms of the GNU General Public License as published by 
     6# the Free Software Foundation; either version 2 of the License, or 
     7# (at your option) any later version. 
     8# 
     9# This program is distributed in the hope that it will be useful, 
     10# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
     12# GNU General Public License for more details. 
     13# 
     14# You should have received a copy of the GNU General Public License 
     15# along with this program; if not, write to the Free Software 
     16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
     17# 
     18 
     19import logging 
     20 
     21_logger = logging.getLogger('ControlPanel - Configuration') 
  • (a) /dev/null vs. (b) b/extensions/cpsection/configuration/view.py

    diff --git a/extensions/cpsection/configuration/view.py b/extensions/cpsection/configuration/view.py
    new file mode 100644
    index 0000000..8389fd3
    a b  
     1# Copyright (C) 2012 Simon Schampijer <erikos@sugarlabs.org> 
     2# Copyright (C) 2012 Ajay Garg        <ajay@activitycentral.com> 
     3# 
     4# This program is free software; you can redistribute it and/or modify 
     5# it under the terms of the GNU General Public License as published by 
     6# the Free Software Foundation; either version 2 of the License, or 
     7# (at your option) any later version. 
     8# 
     9# This program is distributed in the hope that it will be useful, 
     10# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
     12# GNU General Public License for more details. 
     13# 
     14# You should have received a copy of the GNU General Public License 
     15# along with this program; if not, write to the Free Software 
     16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA 
     17 
     18import gtk 
     19import gobject 
     20from gettext import gettext as _ 
     21 
     22from sugar.graphics import style 
     23from sugar.graphics.alert import Alert 
     24from sugar.graphics.icon import Icon 
     25 
     26from jarabe.model import buddy 
     27from jarabe.model import friends 
     28from jarabe.model import neighborhood 
     29 
     30from jarabe.controlpanel.sectionview import SectionView 
     31from jarabe.controlpanel.inlinealert import InlineAlert 
     32 
     33owner_model = buddy.get_owner_instance() 
     34friends_model = friends.get_model() 
     35neighborhood_model = neighborhood.get_model() 
     36 
     37class LeftAlignedLabelWidget(gtk.HBox): 
     38 
     39    def __init__(self, label): 
     40        gtk.HBox.__init__(self) 
     41        label_widget = gtk.Label(label) 
     42        label_widget.set_line_wrap(True) 
     43        self.pack_start(label_widget, expand=False) 
     44        self.show_all() 
     45 
     46 
     47class CenterAlignedLabelWidget(gtk.VBox): 
     48 
     49    def __init__(self, label): 
     50        gtk.VBox.__init__(self) 
     51        label_widget = gtk.Label(label) 
     52        label_widget.set_line_wrap(True) 
     53        self.pack_start(label_widget, expand=False) 
     54        self.show_all() 
     55 
     56 
     57class BuddyWidget(gtk.HBox): 
     58 
     59    def __init__(self, buddy): 
     60        gtk.HBox.__init__(self) 
     61        self._buddy = buddy 
     62 
     63        self._check_button = gtk.CheckButton(label=self._buddy.nick) 
     64        self._check_button.set_active(False) 
     65        self.pack_start(self._check_button, expand=False) 
     66        self.show_all() 
     67 
     68    def _is_buddy_selected(self): 
     69        return self._check_button.get_active() 
     70 
     71    def _get_buddy_key(self): 
     72        return self._buddy.key 
     73 
     74    def _get_buddy_nick(self): 
     75        return self._buddy.nick 
     76 
     77    def _get_buddy_account(self): 
     78        if hasattr(self._buddy, 'account'): 
     79            return self._buddy.account 
     80        return None 
     81 
     82    def _get_buddy_contact_id(self): 
     83        if hasattr(self._buddy, 'contact_id'): 
     84            return self._buddy.contact_id 
     85        return None 
     86 
     87 
     88class AddRemoveWidget(gtk.VBox): 
     89 
     90    def __init__(self, label, group_detail, add_button_clicked_cb, 
     91                 remove_button_clicked_cb, index): 
     92        gtk.VBox.__init__(self) 
     93        self.set_homogeneous(False) 
     94        self.set_spacing(10) 
     95 
     96        self._potential_new_group = False 
     97        self._group_name = label 
     98        self._group_detail = group_detail 
     99 
     100        self._primary_box = gtk.HBox() 
     101        self._primary_box.set_homogeneous(False) 
     102        self._primary_box.set_spacing(10) 
     103        self.pack_start(self._primary_box, expand=False) 
     104        self._primary_box.show_all() 
     105 
     106        self._index = index 
     107        self._add_button_added = False 
     108        self._remove_button_added = False 
     109 
     110        self._label = gtk.Entry() 
     111        self._label.set_text(label) 
     112 
     113        # Do not allow an already existing group name to be modified. 
     114        if len(label) > 0: 
     115            self._label.set_sensitive(False) 
     116        # Else, this is a potentially new group. 
     117        # Mark this is as new. 
     118        # However, this will ACTUALLY be a new group, 
     119        # only if it is given a name. 
     120        # That check will be done, after the user clicks the final save 
     121        # button. 
     122        else: 
     123            self._potential_new_group = True 
     124 
     125        self._primary_box.pack_start(self._label, expand=False) 
     126 
     127        if not self._potential_new_group: 
     128            self._details_button = gtk.Button(_('View Details')) 
     129            self._view_id = self._details_button.connect('clicked', self.__view_details_cb) 
     130            self._primary_box.pack_start(self._details_button, 
     131                    expand=False) 
     132 
     133        add_icon = Icon(icon_name='list-add') 
     134        self._add_button = gtk.Button() 
     135        self._add_button.set_image(add_icon) 
     136        self._add_button.connect('clicked', 
     137                                 add_button_clicked_cb, 
     138                                 self) 
     139 
     140        remove_icon = Icon(icon_name='list-remove') 
     141        self._remove_button = gtk.Button() 
     142        self._remove_button.set_image(remove_icon) 
     143        self._remove_button.connect('clicked', 
     144                                    remove_button_clicked_cb, 
     145                                    self) 
     146 
     147        self.__add_add_button() 
     148        self.__add_remove_button() 
     149 
     150        self._details_table = gtk.VBox() 
     151        self._details_table.set_spacing(20) 
     152        self._details_table.show_all() 
     153        self.pack_start(self._details_table, expand=False) 
     154 
     155        if self._potential_new_group: 
     156            info_label = LeftAlignedLabelWidget(_('You may batch add' 
     157                ' from the following available online buddies. Any' 
     158                ' currently offline buddy may be added later from the' 
     159                ' neighborhood view, when it comes online.')) 
     160            self.pack_start(info_label, expand=False) 
     161            self._friends_list_box = gtk.VBox() 
     162            self._friends_list_box.set_homogeneous(True) 
     163            self._friends_list_box.set_spacing(20) 
     164            self.pack_start(self._friends_list_box, expand=False) 
     165            self._populate_friends_list() 
     166 
     167        self.pack_start(gtk.HSeparator()) 
     168 
     169        self._primary_box.show_all() 
     170        self.show_all() 
     171 
     172    def _populate_friends_list(self): 
     173        buddies = neighborhood_model.get_buddies() 
     174        for buddy in buddies: 
     175 
     176            # Only make the buddy visible, if it has a valid key at 
     177            # this time. 
     178            if ((hasattr(buddy, 'key')) and (buddy.key is not None)): 
     179 
     180                # Do not show self :) 
     181                if buddy.key == owner_model.get_key(): 
     182                    continue 
     183 
     184                self._friends_list_box.pack_start(BuddyWidget(buddy)) 
     185 
     186        self._friends_list_box.show_all() 
     187 
     188    def _get_buddy_widgets(self): 
     189        return self._friends_list_box.get_children() 
     190 
     191    def _get_index(self): 
     192        return self._index 
     193 
     194    def _set_index(self, value): 
     195        self._index = value 
     196 
     197    def _get_entry(self): 
     198        return self._label.get_text() 
     199 
     200    def __add_add_button(self): 
     201        self._primary_box.pack_start(self._add_button, expand=False) 
     202        self._add_button_added = True 
     203 
     204    def _remove_remove_button_if_not_already(self): 
     205        if self._remove_button_added: 
     206            self.__remove_remove_button() 
     207 
     208    def __remove_remove_button(self): 
     209        self._primary_box.remove(self._remove_button) 
     210        self._remove_button_added = False 
     211 
     212    def _add_remove_button_if_not_already(self): 
     213        if not self._remove_button_added: 
     214            self.__add_remove_button() 
     215 
     216    def __add_remove_button(self): 
     217        self._primary_box.pack_start(self._remove_button, expand=False) 
     218        self._remove_button_added = True 
     219 
     220    def __activate_view_id(self): 
     221        self._details_button.disconnect(self._hide_id) 
     222        self._view_id = self._details_button.connect('clicked', 
     223                self.__view_details_cb) 
     224 
     225    def __activate_hide_id(self): 
     226        self._details_button.disconnect(self._view_id) 
     227        self._hide_id = self._details_button.connect('clicked', 
     228                self.__hide_details_cb) 
     229 
     230    def __view_details_cb(self, widget): 
     231        if self._group_detail is None: 
     232            return 
     233 
     234        last_operation_value = friends_model._get_last_group_operation(self._group_name) 
     235 
     236        self._last_operation_box = gtk.VBox() 
     237        self._last_operation_box.pack_start(LeftAlignedLabelWidget(_('Last' 
     238            ' Operation On This Group :: ')), expand=False) 
     239        self._last_operation_box.pack_start(CenterAlignedLabelWidget( 
     240            last_operation_value), expand=False) 
     241        self._last_operation_box.show_all() 
     242 
     243        self._details_table.pack_start(self._last_operation_box, 
     244                expand=False) 
     245 
     246        self._container = gtk.Table() 
     247        self._details_table.pack_start(self._container, expand=False) 
     248 
     249        headings_list = [_('Nick'), _('Last Operation Status')] 
     250        for i in range(0, len(headings_list)): 
     251            self._container.attach(gtk.Label(headings_list[i]), i, i+1, 
     252                    0, 1) 
     253        for i in range(0, len(headings_list)): 
     254            self._container.attach(gtk.Label(''), i, i+1, 1, 2) 
     255 
     256        index = 2 
     257        friend_keys_of_group = \ 
     258                friends_model._get_friend_keys_of_group(self._group_name) 
     259 
     260        for friend_key in friend_keys_of_group: 
     261            friend_model = friends_model._get_friend_by_key(friend_key) 
     262            label_widgets = [] 
     263            nick = friend_model.get_nick() 
     264            label_widgets.append(CenterAlignedLabelWidget(nick)) 
     265 
     266            last_operation_status = \ 
     267                    friends_model._get_last_operation_status_of_friend_in_group(self._group_name, 
     268                                                                                friend_key) 
     269            label_widgets.append(CenterAlignedLabelWidget(last_operation_status)) 
     270 
     271            for i in range(0, len(label_widgets)): 
     272                self._container.attach(label_widgets[i], i, i+1, index, 
     273                        index+1) 
     274            index = index + 1 
     275 
     276        self._container.show_all() 
     277 
     278        self._details_button.set_label(_('Hide Details')) 
     279        self.__activate_hide_id() 
     280 
     281    def __hide_details_cb(self, widget): 
     282        self._details_table.remove(self._last_operation_box) 
     283        self._details_table.remove(self._container) 
     284 
     285        self._details_button.set_label(_('View Details')) 
     286        self.__activate_view_id() 
     287 
     288 
     289class MultiWidget(gtk.VBox): 
     290 
     291    def __init__(self): 
     292        gtk.VBox.__init__(self) 
     293        self.set_spacing(10) 
     294 
     295    def _add_widget(self, label, metadata): 
     296        new_widget = AddRemoveWidget(label, 
     297                                     metadata, 
     298                                     self.__add_button_clicked_cb, 
     299                                     self.__remove_button_clicked_cb, 
     300                                     len(self.get_children())) 
     301        self.add(new_widget) 
     302        self.show_all() 
     303        self._update_remove_button_statuses() 
     304 
     305    def _add_blank_entry(self): 
     306        self._add_widget('', None) 
     307 
     308    def __add_button_clicked_cb(self, add_button, 
     309                                      add_button_container): 
     310        self._add_blank_entry() 
     311        self._update_remove_button_statuses() 
     312 
     313    def __remove_button_clicked_cb(self, remove_button, 
     314                                   remove_button_container): 
     315        # Remove group from the model. 
     316        group_name = remove_button_container._get_entry() 
     317        friends_model.remove_group(group_name) 
     318 
     319        # Remove group from the view. 
     320        self.remove(remove_button_container) 
     321 
     322        self._update_remove_button_statuses() 
     323 
     324    def _update_remove_button_statuses(self): 
     325        children = self.get_children() 
     326 
     327        # Now, if there is only one entry, remove-button 
     328        # should not be shown. 
     329        if len(children) == 1: 
     330            children[0]._remove_remove_button_if_not_already() 
     331 
     332        # Alternatively, if there are more than 1 entries, 
     333        # remove-button should be shown for all. 
     334        if len(children) > 1: 
     335            for child in children: 
     336                child._add_remove_button_if_not_already() 
     337 
     338    def set_groups(self, groups): 
     339        self._groups = groups 
     340 
     341    def _pre_save_operations(self): 
     342        for child in self.get_children(): 
     343            if child._potential_new_group: 
     344                group_name = child._get_entry() 
     345                if len(group_name) > 0: 
     346                    friends_model.add_group(group_name, False) 
     347 
     348                    # Also, add all the selected buddies as friends. 
     349                    buddy_widgets = child._get_buddy_widgets() 
     350                    for widget in buddy_widgets: 
     351                        if widget._is_buddy_selected(): 
     352                            # Add as friend. 
     353                            friends_model.make_friend_by_parameters( 
     354                                    widget._get_buddy_key(), 
     355                                    widget._get_buddy_nick(), 
     356                                    widget._get_buddy_account(), 
     357                                    widget._get_buddy_contact_id()) 
     358 
     359                            # Add as friend in group. 
     360                            friends_model.add_friend_to_group(widget._get_buddy_key(), 
     361                                                              group_name, 
     362                                                              False) 
     363 
     364        # Perform just one disk-write for groups. 
     365        friends_model.save_groups() 
     366 
     367 
     368class Configuration(SectionView): 
     369    def __init__(self, model, alerts=None): 
     370        SectionView.__init__(self) 
     371 
     372        self._model = model 
     373 
     374        self.set_border_width(style.DEFAULT_SPACING * 2) 
     375        self.set_spacing(style.DEFAULT_SPACING) 
     376        group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) 
     377 
     378        workspace = gtk.VBox() 
     379        workspace.show() 
     380 
     381        separator = gtk.HSeparator() 
     382        workspace.pack_start(separator, expand=False) 
     383 
     384        label_friend_groups = gtk.Label(_('Groups configuration')) 
     385        label_friend_groups.set_alignment(0, 0) 
     386        workspace.pack_start(label_friend_groups, expand=False) 
     387 
     388        box_friend_groups = gtk.VBox() 
     389        box_friend_groups.set_border_width(style.DEFAULT_SPACING * 2) 
     390        box_friend_groups.set_spacing(style.DEFAULT_SPACING) 
     391 
     392        self._widget_table = MultiWidget() 
     393        box_friend_groups.pack_start(self._widget_table, expand=False) 
     394 
     395        save_button = gtk.Button() 
     396        save_button.set_alignment(0, 0) 
     397        save_button.set_label('Save') 
     398        save_button.connect('clicked', self.__save_button_clicked_cb) 
     399        box_save_button = gtk.HBox() 
     400        box_save_button.set_homogeneous(False) 
     401        box_save_button.pack_start(save_button, expand=False) 
     402        box_save_button.show_all() 
     403 
     404        box_friend_groups.pack_start(box_save_button, expand=False) 
     405 
     406        box_friend_groups.show_all() 
     407        workspace.pack_start(box_friend_groups, expand=False) 
     408 
     409        scrolled = gtk.ScrolledWindow() 
     410        scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) 
     411        scrolled.add_with_viewport(workspace) 
     412        scrolled.show() 
     413        self.add(scrolled) 
     414 
     415        workspace.show_all() 
     416        self.setup() 
     417 
     418    def setup(self): 
     419        groups = friends_model._get_groups() 
     420        groups.sort() 
     421 
     422        if len(groups) == 0: 
     423            self._widget_table._add_blank_entry() 
     424        else: 
     425            for group_name in groups: 
     426                group_detail = \ 
     427                        friends_model._get_group_by_key_name(group_name) 
     428                self._widget_table._add_widget(group_name, group_detail) 
     429 
     430    def __save_button_clicked_cb(self, save_button): 
     431        save_button.set_sensitive(False) 
     432        self._widget_table._pre_save_operations() 
  • src/jarabe/frame/activitiestray.py

    diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py
    index 941b174..d2902be 100644
    a b  
    354354    def remove(self): 
    355355        frame = jarabe.frame.get_view() 
    356356        frame.remove_notification(self.notif_icon) 
    357         self.props.parent.remove(self) 
     357        if (self.props.parent is not None) and \ 
     358                (self in self.props.parent.get_children()): 
     359            self.props.parent.remove(self) 
    358360 
    359361    def __notify_state_cb(self, file_transfer, pspec): 
    360362        logging.debug('_update state: %r %r', file_transfer.props.state, 
     
    471473        frame.add_notification(self.notif_icon, 
    472474                               gtk.CORNER_TOP_LEFT) 
    473475 
     476        # TODO: figure out why this is necessary to do. 
     477        #       if this step is not done, then invoking 
     478        #       "__dismiss_clicked_cb()" WITHOUT clicking the "Dismiss" 
     479        #       option (as in the case of auto-dismiss for bulk 
     480        #       operations), DOES NOT WORK. 
     481        self.create_palette() 
     482 
    474483    def create_palette(self): 
    475484        palette = OutgoingTransferPalette(self.file_transfer) 
    476485        palette.connect('dismiss-clicked', self.__dismiss_clicked_cb) 
     
    480489 
    481490    def __dismiss_clicked_cb(self, palette): 
    482491        self.remove() 
     492        bulk_operation_details = \ 
     493                self.file_transfer._get_bulk_operation_details() 
     494        if bulk_operation_details is not None: 
     495            group_name = bulk_operation_details._get_group_name() 
     496            friend_keys = bulk_operation_details._get_friend_keys() 
     497            counter = bulk_operation_details._get_counter() 
     498            proceed_cb = bulk_operation_details._get_proceed_cb() 
     499            proceed_cb(group_name, friend_keys, counter) 
    483500 
    484501 
    485502class BaseTransferPalette(Palette): 
     
    565582 
    566583    def _update(self): 
    567584        logging.debug('_update state: %r', self.file_transfer.props.state) 
     585 
    568586        if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING: 
    569587            menu_item = MenuItem(_('Accept'), icon_name='dialog-ok') 
    570588            menu_item.connect('activate', self.__accept_activate_cb) 
     
    595613 
    596614        elif self.file_transfer.props.state in \ 
    597615                [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]: 
    598  
    599616            for item in self.menu.get_children(): 
    600617                self.menu.remove(item) 
    601618 
     
    619636            self.update_progress() 
    620637 
    621638        elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED: 
    622  
    623639            for item in self.menu.get_children(): 
    624640                self.menu.remove(item) 
    625641 
     
    629645            menu_item.show() 
    630646 
    631647            self.update_progress() 
    632         elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: 
    633648 
     649        elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED: 
    634650            for item in self.menu.get_children(): 
    635651                self.menu.remove(item) 
    636652 
     
    684700 
    685701        self.progress_bar = None 
    686702        self.progress_label = None 
     703        self._bulk_operation_details = \ 
     704                file_transfer._get_bulk_operation_details() 
    687705 
    688706        self.file_transfer.connect('notify::state', self.__notify_state_cb) 
    689707 
    690708        nick = str(file_transfer.buddy.props.nick) 
    691         label = glib.markup_escape_text(_('Transfer to %s') % (nick,)) 
     709 
     710        label = None 
     711        if self._bulk_operation_details is None: 
     712            label = glib.markup_escape_text(_('Transfer to %s') % (nick,)) 
     713        else: 
     714            counter = self._bulk_operation_details._get_counter() 
     715            total = self._bulk_operation_details._get_total() 
     716            label = glib.markup_escape_text(_('( %d / %d ) Transfer to %s') \ 
     717                    % (counter, total, nick,)) 
    692718        self.props.secondary_text = label 
    693719 
    694720        self._update() 
     
    699725    def _update(self): 
    700726        new_state = self.file_transfer.props.state 
    701727        logging.debug('_update state: %r', new_state) 
    702         if new_state == filetransfer.FT_STATE_PENDING: 
    703728 
     729        if new_state == filetransfer.FT_STATE_PENDING: 
    704730            menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel') 
    705731            menu_item.connect('activate', self.__cancel_activate_cb) 
    706732            self.menu.append(menu_item) 
     
    725751 
    726752        elif new_state in [filetransfer.FT_STATE_ACCEPTED, 
    727753                           filetransfer.FT_STATE_OPEN]: 
    728  
    729754            for item in self.menu.get_children(): 
    730755                self.menu.remove(item) 
    731756 
     
    750775 
    751776        elif new_state in [filetransfer.FT_STATE_COMPLETED, 
    752777                           filetransfer.FT_STATE_CANCELLED]: 
    753  
    754778            for item in self.menu.get_children(): 
    755779                self.menu.remove(item) 
    756780 
     
    758782            menu_item.connect('activate', self.__dismiss_activate_cb) 
    759783            self.menu.append(menu_item) 
    760784            menu_item.show() 
    761  
    762785            self.update_progress() 
    763786 
     787            # Perform actions specific to bulk-operation. 
     788            if self._bulk_operation_details is not None: 
     789 
     790                # Update the peration status. 
     791                status = None 
     792                if new_state == filetransfer.FT_STATE_COMPLETED: 
     793                    status = _('Success.') 
     794                elif new_state == filetransfer.FT_STATE_CANCELLED: 
     795                    if self.file_transfer.reason_last_change == \ 
     796                            filetransfer.FT_REASON_REMOTE_STOPPED: 
     797                        status = _('FAILURE:\tOperation Cancelled Remotely.') 
     798                    elif self.file_transfer.reason_last_change == \ 
     799                            filetransfer.FT_REASON_LOCAL_STOPPED: 
     800                        status = _('FAILURE:\tOperation Cancelled Locally.') 
     801 
     802                self._set_bulk_operation_status_if_applicable(status) 
     803 
     804                # "Dismiss' automatically for all, except the last of 
     805                # the bulk-operation. 
     806                counter = self._bulk_operation_details._get_counter() 
     807                total = self._bulk_operation_details._get_total() 
     808                if counter < total: 
     809                    self.__dismiss_activate_cb(None) 
     810 
     811    def _set_bulk_operation_status_if_applicable(self, status): 
     812        if self._bulk_operation_details is not None: 
     813            self._bulk_operation_details._set_operation_status(status) 
     814 
    764815    def __cancel_activate_cb(self, menu_item): 
    765816        self.file_transfer.cancel() 
    766817 
  • src/jarabe/journal/palettes.py

    diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
    index 8fc1e5d..e1d1e7d 100644
    a b  
    3737from jarabe.journal import misc 
    3838from jarabe.journal import model 
    3939 
     40friends_model = friends.get_model() 
     41 
     42 
     43class BulkOperationDetails(): 
     44 
     45    def __init__(self, group_name, friend, friend_keys, total, counter, proceed_cb): 
     46        self._group_name = group_name 
     47        self._friend = friend 
     48        self._friend_keys = friend_keys 
     49        self._counter = counter 
     50        self._total = total 
     51        self._proceed_cb = proceed_cb 
     52 
     53    def _get_group_name(self): 
     54        return self._group_name 
     55 
     56    def _get_friend_keys(self): 
     57        return self._friend_keys 
     58 
     59    def _get_counter(self): 
     60        return self._counter 
     61 
     62    def _get_total(self): 
     63        return self._total 
     64 
     65    def _get_proceed_cb(self): 
     66        return self._proceed_cb 
     67 
     68    def _set_operation_status(self, status): 
     69        friends_model._set_last_operation_status_of_friend_in_group(self._group_name, 
     70                                                                    self._friend.get_key(), 
     71                                                                    status) 
     72 
    4073 
    4174class ObjectPalette(Palette): 
    4275 
     
    117150        friends_menu.connect('friend-selected', self.__friend_selected_cb) 
    118151        menu_item.set_submenu(friends_menu) 
    119152 
     153        groups_menu = GroupsMenu() 
     154        groups_menu.connect('group-selected', self.__group_selected_cb) 
     155        friends_menu._set_group_menu(groups_menu) 
     156 
    120157        if detail == True: 
    121158            menu_item = MenuItem(_('View Details'), 'go-right') 
    122159            menu_item.connect('activate', self.__detail_activate_cb) 
     
    150187    def __volume_error_cb(self, menu_item, message, severity): 
    151188        self.emit('volume-error', message, severity) 
    152189 
    153     def __friend_selected_cb(self, menu_item, buddy): 
     190    def __friend_selected_cb(self, menu_item, buddy, 
     191            bulk_operation_details=None): 
    154192        logging.debug('__friend_selected_cb') 
    155193        file_name = model.get_file(self._metadata['uid']) 
    156194 
     
    167205 
    168206        if not mime_type: 
    169207            mime_type = mime.get_for_file(file_name) 
    170  
    171208        filetransfer.start_transfer(buddy, file_name, title, description, 
    172                                     mime_type) 
     209                                    mime_type, bulk_operation_details) 
     210 
     211    def __group_selected_cb(self, menu_item, group_name): 
     212        logging.debug('__group_selected_cb') 
     213        if group_name is not None: 
     214            friends_model._set_last_group_operation(group_name, 
     215                    _('(TRANSFER) %s') % (self._metadata['title'],)) 
     216            friends_model._set_last_operation_status_of_friends_in_group_with_common_status( 
     217                    group_name, _('PENDING ...')) 
     218            friend_keys = \ 
     219                    friends_model._get_friend_keys_of_group(group_name) 
     220 
     221            self._proceed_with_next_friend(group_name, friend_keys, 0) 
     222 
     223    """ 
     224    This is the (callback) function that needs to be called per friend. 
     225    Note that this function is a callback (and not a looped one), since 
     226    the "next" friend iteration begins, only when the current iteration 
     227    has finished - which is asynchronous. 
     228    """ 
     229    def _proceed_with_next_friend(self, group_name, friend_keys, counter): 
     230        counter = counter + 1 
     231 
     232        if counter <= len(friend_keys): 
     233            friend_key = friend_keys[counter-1] 
     234            friend = friends_model._get_friend_by_key(friend_key) 
     235 
     236            bulk_operation_details = \ 
     237                    BulkOperationDetails(group_name, 
     238                                         friend, 
     239                                         friend_keys, 
     240                                         len(friend_keys), 
     241                                         counter, 
     242                                         self._proceed_with_next_friend) 
     243 
     244            # Only proceed if the friend is online. 
     245            # Else, set the failure-status, and move forward. 
     246            if friend.is_present(): 
     247                self.__friend_selected_cb(None, friend, bulk_operation_details) 
     248            else: 
     249                bulk_operation_details._set_operation_status(_('FAILURE:\tFriend' 
     250                                                           ' is offline.')) 
     251                self._proceed_with_next_friend(group_name, friend_keys, 
     252                                               counter) 
    173253 
    174254 
    175255class CopyMenu(gtk.Menu): 
     
    295375        self._temp_file_path = None 
    296376 
    297377 
     378class GroupsMenu(gtk.Menu): 
     379    __gtype_name__ = 'GroupsMenu' 
     380 
     381    __gsignals__ = { 
     382        'group-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, 
     383                            ([object])), 
     384    } 
     385 
     386    def __init__(self): 
     387        gobject.GObject.__init__(self) 
     388 
     389        if filetransfer.file_transfer_available(): 
     390            for group in friends_model._get_groups(): 
     391                menu_item = MenuItem(text_label=group, 
     392                                     icon_name='zoom-groups') 
     393                menu_item.connect('activate', self.__item_activate_cb, 
     394                                  group) 
     395                self.append(menu_item) 
     396                menu_item.show() 
     397 
     398            if not self.get_children(): 
     399                menu_item = MenuItem(_('No groups present')) 
     400                menu_item.set_sensitive(False) 
     401                self.append(menu_item) 
     402                menu_item.show() 
     403        else: 
     404            menu_item = MenuItem(_('No valid connection found')) 
     405            menu_item.set_sensitive(False) 
     406            self.append(menu_item) 
     407            menu_item.show() 
     408 
     409    def __item_activate_cb(self, menu_item, group): 
     410        self.emit('group-selected', group) 
     411 
     412 
    298413class FriendsMenu(gtk.Menu): 
    299414    __gtype_name__ = 'JournalFriendsMenu' 
    300415 
     
    307422        gobject.GObject.__init__(self) 
    308423 
    309424        if filetransfer.file_transfer_available(): 
    310             friends_model = friends.get_model() 
    311425            for friend in friends_model: 
    312426                if friend.is_present(): 
    313427                    menu_item = MenuItem(text_label=friend.get_nick(), 
     
    332446    def __item_activate_cb(self, menu_item, friend): 
    333447        self.emit('friend-selected', friend) 
    334448 
     449    def _set_group_menu(self, group_menu): 
     450        menu_item = MenuItem(_('Select a group, to send')) 
     451        menu_item.set_submenu(group_menu) 
     452        self.append(menu_item) 
     453        menu_item.show() 
     454 
    335455 
    336456class StartWithMenu(gtk.Menu): 
    337457    __gtype_name__ = 'JournalStartWithMenu' 
  • src/jarabe/model/buddy.py

    diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py
    index 8f17d7e..c6c6a4c 100644
    a b  
    4343        self._color = None 
    4444        self._tags = None 
    4545        self._current_activity = None 
     46        self._groups = None 
    4647 
    4748        gobject.GObject.__init__(self, **kwargs) 
    4849 
     
    8788                                        getter=get_current_activity, 
    8889                                        setter=set_current_activity) 
    8990 
     91    def get_groups(self): 
     92        return self._groups 
     93 
     94    def set_groups(self, groups): 
     95        self._groups = groups 
     96 
     97    groups = gobject.property(type=object, 
     98                              getter=get_groups, 
     99                              setter=set_groups) 
     100 
    90101    def is_owner(self): 
    91102        raise NotImplementedError 
    92103 
     
    179190        self._account = None 
    180191        self._contact_id = None 
    181192        self._handle = None 
     193        self._groups_list = [] 
    182194 
    183195        BaseBuddyModel.__init__(self, **kwargs) 
    184196 
  • src/jarabe/model/filetransfer.py

    diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py
    index 447a74a..3de8c20 100644
    a b  
    124124        self.mime_type = None 
    125125        self.initial_offset = 0 
    126126        self.reason_last_change = FT_REASON_NONE 
     127        self._bulk_operation_details = None 
    127128 
    128129    def set_channel(self, channel): 
    129130        self.channel = channel 
     
    156157    def _get_transferred_bytes(self): 
    157158        return self._transferred_bytes 
    158159 
     160    def _get_bulk_operation_details(self): 
     161        return self._bulk_operation_details 
     162 
     163    def _set_bulk_operation_details(self, bulk_operation_details): 
     164        self._bulk_operation_details = bulk_operation_details 
     165 
    159166    transferred_bytes = gobject.property(type=int, default=0, 
    160167            getter=_get_transferred_bytes, setter=_set_transferred_bytes) 
    161168 
     
    226233 
    227234 
    228235class OutgoingFileTransfer(BaseFileTransfer): 
    229     def __init__(self, buddy, file_name, title, description, mime_type): 
     236    def __init__(self, buddy, file_name, title, description, mime_type, 
     237            bulk_operation_details): 
    230238 
    231239        presence_service = presenceservice.get_instance() 
    232240        name, path = presence_service.get_preferred_connection() 
     
    241249        self._socket = None 
    242250        self._splicer = None 
    243251        self._output_stream = None 
     252        self._set_bulk_operation_details(bulk_operation_details) 
    244253 
    245254        self.buddy = buddy 
    246255        self.title = title 
     
    324333        _monitor_connection(connection) 
    325334 
    326335 
    327 def start_transfer(buddy, file_name, title, description, mime_type): 
     336def start_transfer(buddy, file_name, title, description, mime_type, 
     337                   bulk_operation_details): 
    328338    outgoing_file_transfer = OutgoingFileTransfer(buddy, file_name, title, 
    329                                                   description, mime_type) 
     339                                                  description, 
     340                                                  mime_type, 
     341                                                  bulk_operation_details) 
    330342    new_file_transfer.send(None, file_transfer=outgoing_file_transfer) 
    331343 
    332344 
  • src/jarabe/model/friends.py

    diff --git a/src/jarabe/model/friends.py b/src/jarabe/model/friends.py
    index 7605af1..448a36e 100644
    a b  
    1414# along with this program; if not, write to the Free Software 
    1515# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
    1616 
     17from gettext import gettext as _ 
     18 
    1719import os 
    1820import logging 
    1921from ConfigParser import ConfigParser 
     
    2224import dbus 
    2325 
    2426from sugar import env 
     27from sugar.util import unique_id 
    2528from sugar.graphics.xocolor import XoColor 
    2629 
    2730from jarabe.model.buddy import BuddyModel 
     
    3639 
    3740    _NOT_PRESENT_COLOR = '#D5D5D5,#FFFFFF' 
    3841 
    39     def __init__(self, nick, key, account=None, contact_id=None): 
     42    def __init__(self, nick, key, account=None, contact_id=None, groups=[]): 
    4043        self._online_buddy = None 
    4144 
    4245        BuddyModel.__init__(self, nick=nick, key=key, account=account, 
    43                             contact_id=contact_id) 
     46                            contact_id=contact_id, groups=groups) 
    4447 
    4548        neighborhood_model = neighborhood.get_model() 
    4649        neighborhood_model.connect('buddy-added', self.__buddy_added_cb) 
     
    110113 
    111114    def __init__(self): 
    112115        gobject.GObject.__init__(self) 
    113  
    114         self._friends = {} 
    115116        self._path = os.path.join(env.get_profile_path(), 'friends') 
     117        self._groups_path = os.path.join(env.get_profile_path(), 'groups') 
     118        self.reinit() 
    116119 
     120    def reinit(self): 
     121        self._friends = {} 
     122        self._groups = {} 
    117123        self.load() 
    118124 
    119125    def has_buddy(self, buddy): 
    120         return buddy.get_key() in self._friends 
     126        return self.check_buddy_existence_by_key(buddy.get_key()) 
     127 
     128    """ 
     129    Ideally, this should be the only publically exposed API, since 
     130    only the buddy_key is the deciding factor of the existence of 
     131    a friend. There can never be two friends of the same key. 
     132    """ 
     133    def check_buddy_existence_by_key(self, buddy_key): 
     134        return buddy_key in self._friends 
    121135 
    122136    def add_friend(self, buddy_info): 
    123137        self._friends[buddy_info.get_key()] = buddy_info 
    124138        self.emit('friend-added', buddy_info) 
    125139 
    126     def make_friend(self, buddy): 
    127         if not self.has_buddy(buddy): 
    128             buddy = FriendBuddyModel(key=buddy.key, nick=buddy.nick, 
    129                                      account=buddy.account, 
    130                                      contact_id=buddy.contact_id) 
     140    def add_friend_to_group(self, buddy_key, group_name, 
     141                            save_group=True): 
     142        if not self.__group_exists(group_name): 
     143            return _('Cannot add to group !! Create this group first !!') 
     144 
     145        if self.__friend_exists_in_group(group_name, buddy_key): 
     146            return _('Friend already exists in this group') 
     147 
     148        # If we reach here, it is safe to add this buddy :) 
     149        self._groups[group_name]['friends'][buddy_key] = {} 
     150 
     151        # Also, update the association from the buddy side, and save. 
     152        groups = self._get_groups_of_a_friend(buddy_key) 
     153        if group_name not in groups: 
     154            groups.append(group_name) 
     155            groups.sort() 
     156            self.save() 
     157 
     158        if save_group: 
     159            self.save_groups() 
     160 
     161    def remove_friend_group_assoc_using_friend_key(self, group_name, 
     162            friend_key, save_friend=True, save_group=True): 
     163        if not self.__group_exists(group_name): 
     164            return _('No such group exists !!') 
     165 
     166        # Remove association from friend. 
     167        if group_name in self._friends[friend_key].get_groups(): 
     168            self._friends[friend_key].get_groups().remove(group_name) 
     169 
     170        # Remove association from group. 
     171        if friend_key in self._groups[group_name]['friends'].keys(): 
     172            del self._groups[group_name]['friends'][friend_key] 
     173 
     174        if save_friend: 
     175            self.save() 
     176        if save_group: 
     177            self.save_groups() 
     178 
     179    def _get_groups_of_a_friend(self, friend_key): 
     180        if friend_key not in self._friends.keys(): 
     181            return [] 
     182 
     183        return self._friends[friend_key].get_groups() 
     184 
     185    def add_group(self, group_name, save_group=True): 
     186        if self.__group_exists(group_name): 
     187            return _('A group with the same name already exists !!' 
     188                     'Choose a different group-name.') 
     189 
     190        # If we reach here, it is safe to add a group of this name. 
     191        self._groups[group_name] = {} 
     192        self._groups[group_name]['friends'] = {} 
     193        self._groups[group_name]['last-operation'] = 'NONE.' 
     194        if save_group: 
     195            self.save_groups() 
     196 
     197    def remove_group(self, group_name, save_friend=True, 
     198            save_group=True): 
     199        if not self.__group_exists(group_name): 
     200            return _('No group exists of this name !!') 
     201 
     202        # Remove this group, from all the friend-associations. 
     203        for friend_key in self._friends.keys(): 
     204            friend_groups_list = self._friends[friend_key].get_groups() 
     205            if group_name in friend_groups_list: 
     206                friend_groups_list.remove(group_name) 
     207        if save_friend: 
     208            self.save() 
     209 
     210        # Remove the group itself. 
     211        del self._groups[group_name] 
     212        if save_group: 
     213            self.save_groups() 
     214 
     215    def __group_exists(self, group_name): 
     216        if group_name in self._groups.keys(): 
     217            return True 
     218        return False 
     219 
     220    def __friend_exists_in_group(self, group_name, friend_key): 
     221        if friend_key in \ 
     222                self._groups[group_name]['friends'].keys(): 
     223            return True 
     224        return False 
     225 
     226    """ 
     227    This method is useful in the case, when the buddy is to be added as 
     228    a friend in deferred sense. For example, the buddies list may first be 
     229    fetched by virtue of their being online, but may go offline by the 
     230    time they are about to be saved. So, in that case, we retrieve the 
     231    parameters that are required for saving in the early stages itself. 
     232    """ 
     233    def make_friend_by_parameters(self, buddy_key, buddy_nick, buddy_account, buddy_contact_id): 
     234        if not self.check_buddy_existence_by_key(buddy_key): 
     235            buddy = FriendBuddyModel(key=buddy_key, 
     236                                     nick=buddy_nick, 
     237                                     account=buddy_account, 
     238                                     contact_id=buddy_contact_id) 
    131239            self.add_friend(buddy) 
    132240            self.save() 
    133241 
     242    def make_friend(self, buddy): 
     243        self.make_friend_by_parameters(buddy.key, buddy.nick, 
     244                                       buddy.account, buddy.contact_id) 
     245 
     246    def remove_groups_of_friend(self, buddy_info, save_friend=True, 
     247            save_group=True): 
     248        groups = self._get_groups_of_a_friend(buddy_info.get_key()) 
     249        for group in groups: 
     250            self.remove_friend_group_assoc_using_friend_key(group, 
     251                    buddy_info.get_key(), False, False) 
     252 
     253        if save_friend: 
     254            self.save() 
     255        if save_group: 
     256            self.save_groups() 
     257 
    134258    def remove(self, buddy_info): 
     259        # First remove its association from all its groups 
     260        self.remove_groups_of_friend(buddy_info, False, True) 
     261 
     262        # Now, remove the friend-entity itself. 
    135263        del self._friends[buddy_info.get_key()] 
    136264        self.save() 
     265 
    137266        self.emit('friend-removed', buddy_info.get_key()) 
    138267 
    139268    def __iter__(self): 
    140269        return self._friends.values().__iter__() 
    141270 
     271    def _get_friend_by_key(self, key): 
     272        if not (key in self._friends.keys()): 
     273            return _('No friend found !!') 
     274        return self._friends[key] 
     275 
     276    def _get_friend_keys_of_group(self, group_name): 
     277        if not self.__group_exists(group_name): 
     278            return [] 
     279 
     280        return self._groups[group_name]['friends'].keys() 
     281 
     282    def _get_groups(self): 
     283        # Sascha's wonderful feedback: always export only the thing 
     284        # required. Here, we required only the group-names; so export 
     285        # just the keys 
     286        return self._groups.keys() 
     287 
     288    def _set_groups(self, groups): 
     289        self._groups = groups 
     290 
     291    def _get_group_by_key_name(self, group_name): 
     292        if not self.__group_exists(group_name): 
     293            return None 
     294        return self._groups[group_name] 
     295 
     296    def _get_last_group_operation(self, group_name): 
     297        if not self.__group_exists(group_name): 
     298            return _('No group of this name exists !!') 
     299        return self._groups[group_name]['last-operation'] 
     300 
     301    def _set_last_group_operation(self, group_name, operation): 
     302        if not self.__group_exists(group_name): 
     303            return _('No group exists.') 
     304        self._groups[group_name]['last-operation'] = operation 
     305        self.save_groups 
     306 
     307    def _get_last_operation_status_of_friend_in_group(self, group_name, 
     308                                                      friend_key): 
     309        if not self.__group_exists(group_name): 
     310            return _('No group exists.') 
     311 
     312        if not self.__friend_exists_in_group(group_name, friend_key): 
     313            return _('Friend does not exist in group') 
     314 
     315        if 'last-operation-status' in \ 
     316                self._groups[group_name]['friends'][friend_key].keys(): 
     317            return self._groups[group_name]['friends'][friend_key]['last-operation-status'] 
     318 
     319    def _set_last_operation_status_of_friend_in_group(self, group_name, 
     320                                                      friend_key, status, 
     321                                                      save_group=True): 
     322        if not self.__group_exists(group_name): 
     323            return _('No group exists.') 
     324 
     325        if not self.__friend_exists_in_group(group_name, friend_key): 
     326            return _('Friend does not exist in group') 
     327 
     328        self._groups[group_name]['friends'][friend_key]['last-operation-status'] = \ 
     329                status 
     330 
     331        if save_group: 
     332            self.save_groups() 
     333 
     334    def _set_last_operation_status_of_friends_in_group_with_common_status( 
     335            self, group_name, status): 
     336        if not self.__group_exists(group_name): 
     337            return _('No group exists.') 
     338 
     339        friend_keys = self._get_friend_keys_of_group(group_name) 
     340        for friend_key in friend_keys: 
     341            self._set_last_operation_status_of_friend_in_group(group_name, 
     342                                                               friend_key, 
     343                                                               status, 
     344                                                               False) 
     345 
     346        # Save the groups in one go. 
     347        self.save_groups() 
     348 
    142349    def load(self): 
    143350        cp = ConfigParser() 
    144351 
     
    149356                    # HACK: don't screw up on old friends files 
    150357                    if len(key) < 20: 
    151358                        continue 
    152                     buddy = FriendBuddyModel(key=key, nick=cp.get(key, 'nick')) 
     359 
     360                    # Check for the existence of 'groups' option (for 
     361                    # backwards compatability) 
     362                    groups = [] 
     363                    if cp.has_option(key, 'groups'): 
     364                        groups = eval(cp.get(key, 'groups')) 
     365 
     366                    buddy = FriendBuddyModel(key=key, nick=cp.get(key, 
     367                        'nick'), groups=groups) 
    153368                    self.add_friend(buddy) 
    154369        except Exception: 
    155370            logging.exception('Error parsing friends file') 
    156371 
     372        self.__load_groups() 
     373 
     374    def __load_groups(self): 
     375        cp = ConfigParser() 
     376 
     377        try: 
     378            success = cp.read([self._groups_path]) 
     379            if success: 
     380                for group_name in cp.sections(): 
     381                    self._groups[group_name] = {} 
     382                    self._groups[group_name]['friends'] = \ 
     383                            eval(cp.get(group_name, 'friends')) 
     384                    self._groups[group_name]['last-operation'] = \ 
     385                            cp.get(group_name, 'last-operation') 
     386        except: 
     387            logging.exception('Error while loading config') 
     388 
    157389    def save(self): 
    158390        cp = ConfigParser() 
    159391 
     
    161393            section = friend.get_key() 
    162394            cp.add_section(section) 
    163395            cp.set(section, 'nick', friend.get_nick()) 
     396            cp.set(section, 'groups', friend.get_groups()) 
    164397 
    165398        fileobject = open(self._path, 'w') 
    166399        cp.write(fileobject) 
    167400        fileobject.close() 
    168401 
     402    def save_groups(self): 
     403        cp = ConfigParser() 
     404 
     405        for group in self._groups.keys(): 
     406            cp.add_section(group) 
     407            cp.set(group, 'friends', self._groups[group]['friends']) 
     408            cp.set(group, 'last-operation', 
     409                    self._groups[group]['last-operation']) 
     410 
     411        fileobject = open(self._groups_path, 'w') 
     412        cp.write(fileobject) 
     413        fileobject.close() 
    169414 
    170415def get_model(): 
    171416    global _model 
  • src/jarabe/view/buddymenu.py

    diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py
    index de5a772..544faeb 100644
    a b  
    2222import gconf 
    2323import glib 
    2424import dbus 
     25import gobject 
    2526 
    2627from sugar.graphics.palette import Palette 
    2728from sugar.graphics.menuitem import MenuItem 
     
    3334from jarabe.controlpanel.gui import ControlPanel 
    3435import jarabe.desktop.homewindow 
    3536 
     37friends_model = friends.get_model() 
     38 
     39class GroupsMenu(gtk.Menu): 
     40    __gtype_name__ = 'NeighbourhoodGroupsMenu' 
     41 
     42    __gsignals__ = { 
     43        'group-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, 
     44                            ([object])), 
     45    } 
     46 
     47    def __init__(self, groups, no_groups_label): 
     48        gobject.GObject.__init__(self) 
     49 
     50        for group in groups: 
     51            menu_item = MenuItem(text_label=group, icon_name='zoom-groups') 
     52            menu_item.connect('activate', self.__item_activate_cb, group) 
     53            self.append(menu_item) 
     54            menu_item.show() 
     55 
     56        if not self.get_children(): 
     57            menu_item = MenuItem(no_groups_label) 
     58            menu_item.set_sensitive(False) 
     59            self.append(menu_item) 
     60            menu_item.show() 
     61 
     62    def __item_activate_cb(self, menu_item, group): 
     63        self.emit('group-selected', group) 
     64 
    3665 
    3766class BuddyMenu(Palette): 
    3867    def __init__(self, buddy): 
     
    6493 
    6594    def _add_buddy_items(self): 
    6695        if friends.get_model().has_buddy(self._buddy): 
    67             menu_item = MenuItem(_('Remove friend'), 'list-remove') 
     96            menu_item = MenuItem(_('Remove friend, and its association' 
     97                                   ' from all groups'), 'list-remove') 
    6898            menu_item.connect('activate', self._remove_friend_cb) 
     99            self.menu.append(menu_item) 
    69100        else: 
    70             menu_item = MenuItem(_('Make friend'), 'list-add') 
     101            menu_item = MenuItem(_('Make friend (without associating' 
     102                                   ' to any group)'), 'list-add') 
    71103            menu_item.connect('activate', self._make_friend_cb) 
    72  
    73         self.menu.append(menu_item) 
    74         menu_item.show() 
     104            self.menu.append(menu_item) 
     105 
     106        menu_item_2 = MenuItem(_('Make friend (if not already), and add to group'), 
     107                'list-add') 
     108 
     109        all_groups = friends_model._get_groups() 
     110        groups_of_friend = \ 
     111                friends_model._get_groups_of_a_friend(self._buddy.get_key()) 
     112 
     113        # Make a local copy of groups, of which the friend is not 
     114        # associated with. 
     115        groups_not_of_friend = [] 
     116        for group in all_groups: 
     117            groups_not_of_friend.append(group) 
     118        for group in groups_of_friend: 
     119            groups_not_of_friend.remove(group) 
     120 
     121        groups_of_friend.sort() 
     122        groups_not_of_friend.sort() 
     123 
     124        add_groups_menu = GroupsMenu(groups_not_of_friend, _('No groups to' 
     125                                                             ' associate')) 
     126        add_groups_menu.connect('group-selected', 
     127                self.__make_friend_and_add_to_group) 
     128        self.menu.append(menu_item_2) 
     129        menu_item_2.set_submenu(add_groups_menu) 
     130        add_groups_menu.show() 
     131 
     132        menu_item_3 = MenuItem(_('Remove friend from group'), 'list-remove') 
     133        remove_groups_menu = GroupsMenu(groups_of_friend, _('No groups to' 
     134                                                            ' disassociate')) 
     135        remove_groups_menu.connect('group-selected', 
     136                self.__remove_friend_from_group) 
     137        self.menu.append(menu_item_3) 
     138        menu_item_3.set_submenu(remove_groups_menu) 
     139        remove_groups_menu.show() 
    75140 
    76141        self._invite_menu = MenuItem('') 
    77142        self._invite_menu.connect('activate', self._invite_friend_cb) 
    78143        self.menu.append(self._invite_menu) 
    79144 
     145        self.menu.show_all() 
     146 
    80147        home_model = shell.get_model() 
    81148        self._active_activity_changed_hid = home_model.connect( 
    82149                'active-activity-changed', self._cur_activity_changed_cb) 
     
    154221    def __buddy_notify_nick_cb(self, buddy, pspec): 
    155222        self.set_primary_text(glib.markup_escape_text(buddy.props.nick)) 
    156223 
    157     def _make_friend_cb(self, menuitem): 
     224    def _make_friend_cb(self, menuitem=None): 
    158225        friends.get_model().make_friend(self._buddy) 
    159226 
    160227    def _remove_friend_cb(self, menuitem): 
     
    178245                    raise 
    179246        else: 
    180247            logging.error('Invite failed, activity service not ') 
     248 
     249    def __make_friend_and_add_to_group(self, menu_item, group): 
     250        self._make_friend_cb() 
     251        friends_model.add_friend_to_group(self._buddy.get_key(), group) 
     252 
     253    def __remove_friend_from_group(self, menu_item, group): 
     254        friends_model.remove_friend_group_assoc_using_friend_key(group, 
     255                self._buddy.get_key())