Ticket #3315: 0001-sl-3315.patch

File 0001-sl-3315.patch, 68.8 KB (added by ajay_garg, 12 years 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 data/sugar-emulator.desktop 
    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 sugar_DATA = \ 
    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             \
  • new file 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
    - +  
     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 SUBDIRS = \ 
    22        aboutme \
    33        aboutcomputer \
    44        accessibility \
     5        configuration \
    56        datetime \
    67        frame \
    78        keyboard \
  • new file 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
    - +  
     1sugardir = $(pkgdatadir)/extensions/cpsection/configuration
     2
     3sugar_PYTHON =          \
     4        __init__.py     \
     5        model.py        \
     6        view.py         
  • new file 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
    - +  
     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')
  • new file 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
    - +  
     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')
  • new file 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
    - +  
     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 class BaseTransferButton(ToolButton): 
    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,
    class OutgoingTransferButton(BaseTransferButton): 
    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)
    class OutgoingTransferButton(BaseTransferButton): 
    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):
    class IncomingTransferPalette(BaseTransferPalette): 
    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)
    class IncomingTransferPalette(BaseTransferPalette): 
    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
    class IncomingTransferPalette(BaseTransferPalette): 
    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
    class IncomingTransferPalette(BaseTransferPalette): 
    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
    class OutgoingTransferPalette(BaseTransferPalette): 
    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()
    class OutgoingTransferPalette(BaseTransferPalette): 
    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)
    class OutgoingTransferPalette(BaseTransferPalette): 
    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
    class OutgoingTransferPalette(BaseTransferPalette): 
    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
    class OutgoingTransferPalette(BaseTransferPalette): 
    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 from jarabe.model import mimeregistry 
    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
    class ObjectPalette(Palette): 
    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)
    class ObjectPalette(Palette): 
    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
    class ObjectPalette(Palette): 
    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):
    class ClipboardMenu(MenuItem): 
    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
    class FriendsMenu(gtk.Menu): 
    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(),
    class FriendsMenu(gtk.Menu): 
    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 class BaseBuddyModel(gobject.GObject): 
    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
    class BaseBuddyModel(gobject.GObject): 
    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
    class BuddyModel(BaseBuddyModel): 
    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 class BaseFileTransfer(gobject.GObject): 
    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
    class BaseFileTransfer(gobject.GObject): 
    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
    class IncomingFileTransfer(BaseFileTransfer): 
    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()
    class OutgoingFileTransfer(BaseFileTransfer): 
    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
    def init(): 
    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
    import gobject 
    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
    class FriendBuddyModel(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)
    class Friends(gobject.GObject): 
    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
    class Friends(gobject.GObject): 
    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
    class Friends(gobject.GObject): 
    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 import gtk 
    2222import gconf
    2323import glib
    2424import dbus
     25import gobject
    2526
    2627from sugar.graphics.palette import Palette
    2728from sugar.graphics.menuitem import MenuItem
    from jarabe.model.session import get_session_manager 
    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):
    class BuddyMenu(Palette): 
    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)
    class BuddyMenu(Palette): 
    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):
    class BuddyMenu(Palette): 
    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())