r1748 - in trunk: . img/icons img/icons/custom-iconic src/main/java/cc/kune/common/client/resources src/main/java/cc/kune/core/client/resources src/main/java/cc/kune/events/client/actions src/main/java/cc/kune/events/client/viewer src/main/java/cc/kune/events/shared src/main/java/org/waveprotocol/wave/client src/main/java/org/waveprotocol/wave/client/gadget src/main/java/org/waveprotocol/wave/client/gadget/renderer src/main/resources src/main/resources/icons src/main/webapp/others
Author: vjrj_ Date: 2012-03-08 16:05:45 +0100 (Thu, 08 Mar 2012) New Revision: 1748 Added: trunk/img/icons/custom-iconic/ trunk/img/icons/custom-iconic/barters.svg trunk/img/icons/custom-iconic/blog.svg trunk/img/icons/custom-iconic/chain-alternate.svg trunk/img/icons/custom-iconic/chain-close.svg trunk/img/icons/custom-iconic/chain-open.svg trunk/img/icons/custom-iconic/document_stroke-mod.svg trunk/img/icons/custom-iconic/lists.svg trunk/img/icons/custom-iconic/location.svg trunk/img/icons/custom-iconic/picture.svg trunk/img/icons/custom-iconic/reply.svg trunk/img/icons/custom-iconic/wiki_stroke-mod.svg trunk/src/main/java/org/waveprotocol/wave/client/gadget/ trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/ trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/Gadget.css trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidget.java trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.java trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.ui.xml trunk/src/main/resources/icons/ trunk/src/main/resources/icons/chain-close-black.png trunk/src/main/resources/icons/chain-close-white.png trunk/src/main/resources/icons/chain-closed-grey.png trunk/src/main/resources/icons/chain-open-black.png trunk/src/main/resources/icons/chain-open-grey.png trunk/src/main/resources/icons/chain-open-white.png trunk/src/main/resources/icons/picture-black.png trunk/src/main/resources/icons/picture-grey.png trunk/src/main/resources/icons/picture-white.png trunk/src/main/resources/icons/pref-black.png trunk/src/main/resources/icons/pref-grey.png trunk/src/main/resources/icons/pref-white.png Removed: trunk/src/main/java/cc/kune/core/client/resources/pref-black.png trunk/src/main/java/cc/kune/core/client/resources/pref-grey.png trunk/src/main/java/cc/kune/core/client/resources/pref-white.png Modified: trunk/.classpath trunk/pom.xml trunk/src/main/java/cc/kune/common/client/resources/CommonResources.java trunk/src/main/java/cc/kune/core/client/resources/CoreResources.java trunk/src/main/java/cc/kune/events/client/actions/EventAddMenuItem.java trunk/src/main/java/cc/kune/events/client/actions/EventsClientActions.java trunk/src/main/java/cc/kune/events/client/viewer/CalendarViewerPanel.java trunk/src/main/java/cc/kune/events/shared/EventsClientConversionUtil.java trunk/src/main/webapp/others/kune-client-actions.xml Log: Calendar improvements and fixes. Gadget width fix. New icons. Modified: trunk/.classpath =================================================================== --- trunk/.classpath 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/.classpath 2012-03-08 15:05:45 UTC (rev 1748) @@ -22,6 +22,7 @@ <classpathentry kind="var" path="M2_REPO/org/waveprotocol/client/0.3.12/client-0.3.12.jar"/> <classpathentry kind="var" path="M2_REPO/org/waveprotocol/client-common/0.3.12/client-common-0.3.12.jar"/> <classpathentry kind="var" path="M2_REPO/org/waveprotocol/client-common-src/0.3.12/client-common-src-0.3.12.jar" sourcepath="M2_REPO/org/waveprotocol/client-common-src/0.3.12/client-common-src-0.3.12-sources.jar"/> + <classpathentry kind="var" path="M2_REPO/org/waveprotocol/client-scheduler/0.3.12/client-scheduler-0.3.12.jar"/> <classpathentry kind="var" path="M2_REPO/org/waveprotocol/client-scheduler-src/0.3.12/client-scheduler-src-0.3.12.jar" sourcepath="M2_REPO/org/waveprotocol/client-scheduler-src/0.3.12/client-scheduler-src-0.3.12-sources.jar"/> <classpathentry kind="var" path="M2_REPO/org/waveprotocol/client-src/0.3.12/client-src-0.3.12.jar" sourcepath="M2_REPO/org/waveprotocol/client-src/0.3.12/client-src-0.3.12-sources.jar"/> <classpathentry kind="var" path="M2_REPO/org/cobogw/gwt/cobogw/1.3.1/cobogw-1.3.1.jar"/> Added: trunk/img/icons/custom-iconic/barters.svg =================================================================== --- trunk/img/icons/custom-iconic/barters.svg (rev 0) +++ trunk/img/icons/custom-iconic/barters.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + width="32px" + height="32px" + viewBox="0 0 32 32" + enable-background="new 0 0 32 32" + xml:space="preserve" + id="svg2" + inkscape:version="0.48.2 r9819" + sodipodi:docname="barters.svg"><metadata + id="metadata20"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs18" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview16" + showgrid="false" + inkscape:zoom="1" + inkscape:cx="16" + inkscape:cy="16" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /> +<g + id="Layer_1"> +</g> +<g + id="fullscreen_x5F_exit_x5F_32x32"> + <g + id="g6"> + <polygon + points="27.414,24.586 32,20 20,20 20,32 24.586,27.414 29.172,32 32,29.172 " + id="polygon8" + transform="matrix(-1,0,0,1,52,0)" + style="fill:#231f20" /> + <polygon + points="7.414,4.586 2.875,0.043 0.047,2.871 4.586,7.414 0,12 12,12 12,0 " + id="polygon10" + transform="matrix(-1,0,0,-1,32,12)" + style="fill:#231f20" /> + <polygon + points="0,20 4.586,24.586 0,29.172 2.828,32 7.414,27.414 12,32 12,20 " + id="polygon12" + style="fill:#231f20" + transform="matrix(1,0,0,-1,0,52)" /> + <polygon + points="20,12 32,12 27.414,7.414 31.961,2.871 29.133,0.043 24.586,4.586 20,0 " + id="polygon14" + transform="translate(-20,0)" + style="fill:#231f20" /> + </g> +</g> +</svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/blog.svg =================================================================== --- trunk/img/icons/custom-iconic/blog.svg (rev 0) +++ trunk/img/icons/custom-iconic/blog.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32px" + height="32.023px" + viewBox="0 0 32 32.023" + style="enable-background:new 0 0 32 32.023;" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="blog.svg"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview5" + showgrid="true" + inkscape:zoom="5.5975395" + inkscape:cx="16" + inkscape:cy="10.652002" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid3005" /></sodipodi:namedview> + +<path + style="fill:#231f20" + d="M 0 0 L 0 32.03125 L 32 32.03125 L 32 10 L 29.0625 7.0625 C 29.645385 8.2530281 29.96875 9.5850786 29.96875 11 C 29.96875 13.112978 29.25243 15.059907 28.03125 16.59375 C 28.028366 19.812263 28 28.03125 28 28.03125 L 4 28.03125 C 4 28.03125 4 6.5804999 4 3.9375 L 15.4375 3.9375 C 16.965886 2.7328831 18.902941 2.03125 21 2.03125 C 22.424999 2.03125 23.771869 2.3467994 24.96875 2.9375 L 22.03125 0 L 0 0 z " + id="path3" /><path + inkscape:connector-curvature="0" + style="fill:#231f20" + d="M 25.750001,13.115998 22.328,9.6899993 l 0,-3.6670003 -4.023,0 c 0,3.8454005 0,4.507 0,4.507 0,0.602 0.277,1.122 0.695,1.493 l 3.922001,3.921999 z" + id="path7" + sodipodi:nodetypes="ccccsccc" /><path + transform="matrix(3.277217,0,0,3.277217,-40.416133,-58.744255)" + d="m 21.480664,21.282667 a 2.7403326,2.7403326 0 1 1 -5.480665,0 2.7403326,2.7403326 0 1 1 5.480665,0 z" + sodipodi:ry="2.7403326" + sodipodi:rx="2.7403326" + sodipodi:cy="21.282667" + sodipodi:cx="18.740332" + id="path3005" + style="color:#000000;fill:none;stroke:#231f20;stroke-width:1.2205478;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" /></svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/chain-alternate.svg =================================================================== --- trunk/img/icons/custom-iconic/chain-alternate.svg (rev 0) +++ trunk/img/icons/custom-iconic/chain-alternate.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32" + height="32" + viewBox="0 0 32 32" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="chain-close.svg"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview5" + showgrid="true" + inkscape:zoom="11.313708" + inkscape:cx="-14.194054" + inkscape:cy="5.099319" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid2986" + empspacing="5" + visible="true" + enabled="true" + snapvisiblegridlinesonly="true" /></sodipodi:namedview> + +<path + sodipodi:nodetypes="sssssssssssccs" + inkscape:connector-curvature="0" + id="path2994" + d="m 22,18 -12,0 c -1.105,0 -2,0.896 -2,2 l 0,4.084746 C 8,28.501746 11.582,32 16,32 c 4.418,0 8,-3.498254 8,-7.915254 L 24,20 c 0,-1.104 -0.895,-2 -2,-2 z m -2,6.084746 c 0,0 0,3.915254 -4,3.915254 -4,0 -4,-3.656755 -4,-4 0,-0.08475 0,-0.08475 0,-2 l 8,0 c 0,0 0,1.084746 0,2.084746 z" + style="fill:#231f20" /><path + style="fill:#231f20" + d="M 22,14 10,14 C 8.895,14 8,13.104 8,12 L 8,7.915254 C 8,3.498254 11.582,0 16,0 c 4.418,0 8,3.498254 8,7.915254 L 24,12 c 0,1.104 -0.895,2 -2,2 z M 20,7.915254 C 20,7.915254 20,4 16,4 c -4,0 -4,3.656755 -4,4 0,0.08475 0,0.08475 0,2 l 8,0 c 0,0 0,-1.084746 0,-2.084746 z" + id="path2996" + inkscape:connector-curvature="0" + sodipodi:nodetypes="sssssssssssccs" /><rect + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3017" + width="4" + height="12" + x="14" + y="10" /><path + sodipodi:type="arc" + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="path3019" + sodipodi:cx="17" + sodipodi:cy="10" + sodipodi:rx="2" + sodipodi:ry="2" + d="m 19,10 a 2,2 0 1 1 -4,0 2,2 0 1 1 4,0 z" + transform="translate(-1,0)" /><path + transform="translate(-1,12)" + d="m 19,10 a 2,2 0 1 1 -4,0 2,2 0 1 1 4,0 z" + sodipodi:ry="2" + sodipodi:rx="2" + sodipodi:cy="10" + sodipodi:cx="17" + id="path3022" + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" /><rect + style="color:#000000;fill:none;stroke:#231f20;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3813" + width="12" + height="10" + x="-13" + y="2" + ry="1" + rx="0.5" /><rect + rx="0.5" + ry="1" + y="20" + x="-13" + height="10" + width="12" + id="rect3817" + style="color:#000000;fill:none;stroke:#231f20;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /><rect + y="10" + x="-9" + height="12" + width="4" + id="rect3819" + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /><path + transform="translate(-24,0)" + d="m 19,10 a 2,2 0 1 1 -4,0 2,2 0 1 1 4,0 z" + sodipodi:ry="2" + sodipodi:rx="2" + sodipodi:cy="10" + sodipodi:cx="17" + id="path3821" + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" /><path + sodipodi:type="arc" + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="path3823" + sodipodi:cx="17" + sodipodi:cy="10" + sodipodi:rx="2" + sodipodi:ry="2" + d="m 19,10 a 2,2 0 1 1 -4,0 2,2 0 1 1 4,0 z" + transform="translate(-24,12)" /><rect + rx="0.5" + ry="1" + y="2" + x="-33" + height="10" + width="12" + id="rect3825" + style="color:#000000;fill:none;stroke:#231f20;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /><rect + style="color:#000000;fill:none;stroke:#231f20;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3827" + width="12" + height="10" + x="-33" + y="20" + ry="1" + rx="0.5" /><rect + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3829" + width="4" + height="12" + x="-49" + y="10" /><path + sodipodi:type="arc" + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="path3831" + sodipodi:cx="17" + sodipodi:cy="10" + sodipodi:rx="2" + sodipodi:ry="2" + d="m 19,10 a 2,2 0 1 1 -4,0 2,2 0 1 1 4,0 z" + transform="translate(-64,0)" /><path + transform="translate(-64,12)" + d="m 19,10 a 2,2 0 1 1 -4,0 2,2 0 1 1 4,0 z" + sodipodi:ry="2" + sodipodi:rx="2" + sodipodi:cy="10" + sodipodi:cx="17" + id="path3833" + style="color:#000000;fill:#231f20;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:24;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" /></svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/chain-close.svg =================================================================== --- trunk/img/icons/custom-iconic/chain-close.svg (rev 0) +++ trunk/img/icons/custom-iconic/chain-close.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32" + height="32" + viewBox="0 0 32 32" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="chain-close.svg"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview5" + showgrid="true" + inkscape:zoom="11.313708" + inkscape:cx="9.6518354" + inkscape:cy="14.221054" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid2986" + empspacing="5" + visible="true" + enabled="true" + snapvisiblegridlinesonly="true" /></sodipodi:namedview> + +<path + style="fill:#231f20" + d="M 16 0 C 11.582 -5.9211895e-16 8 3.48925 8 7.90625 L 8 12 C 8 13.104 8.895 14 10 14 L 14 14 L 14 18 L 10 18 C 8.895 18 8 18.896 8 20 L 8 24.09375 C 8 28.51075 11.582 32 16 32 C 20.418 32 24 28.51075 24 24.09375 L 24 20 C 24 18.896 23.105 18 22 18 L 18 18 L 18 14 L 22 14 C 23.105 14 24 13.104 24 12 L 24 7.90625 C 24 3.48925 20.418 0 16 0 z M 16 4 C 20 4 20 7.90625 20 7.90625 L 20 10 L 18 10 C 18 8.8954305 17.104569 8 16 8 C 14.895431 8 14 8.8954305 14 10 L 12 10 L 12 8 C 12 7.656755 12 4 16 4 z M 12 22 L 14 22 C 14 23.104569 14.895431 24 16 24 C 17.104569 24 18 23.104569 18 22 L 20 22 L 20 24.09375 C 20 24.09375 20 28 16 28 C 12 28 12 24.343245 12 24 L 12 22 z " + id="path2994" /></svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/chain-open.svg =================================================================== --- trunk/img/icons/custom-iconic/chain-open.svg (rev 0) +++ trunk/img/icons/custom-iconic/chain-open.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32" + height="32" + viewBox="0 0 32 32" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="chain-open.svg"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview5" + showgrid="true" + inkscape:zoom="22.40625" + inkscape:cx="16" + inkscape:cy="16" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid2986" + empspacing="5" + visible="true" + enabled="true" + snapvisiblegridlinesonly="true" /></sodipodi:namedview> + +<path + sodipodi:nodetypes="sssssssssssccs" + inkscape:connector-curvature="0" + id="path2994" + d="m 22,18 -12,0 c -1.105,0 -2,0.896 -2,2 l 0,4.084746 C 8,28.501746 11.582,32 16,32 c 4.418,0 8,-3.498254 8,-7.915254 L 24,20 c 0,-1.104 -0.895,-2 -2,-2 z m -2,6.084746 c 0,0 0,3.915254 -4,3.915254 -4,0 -4,-3.656755 -4,-4 0,-0.08475 0,-0.08475 0,-2 l 8,0 c 0,0 0,1.084746 0,2.084746 z" + style="fill:#231f20" /><path + style="fill:#231f20" + d="M 22,14 10,14 C 8.895,14 8,13.104 8,12 L 8,7.915254 C 8,3.498254 11.582,0 16,0 c 4.418,0 8,3.498254 8,7.915254 L 24,12 c 0,1.104 -0.895,2 -2,2 z M 20,7.915254 C 20,7.915254 20,4 16,4 c -4,0 -4,3.656755 -4,4 0,0.08475 0,0.08475 0,2 l 8,0 c 0,0 0,-1.084746 0,-2.084746 z" + id="path2996" + inkscape:connector-curvature="0" + sodipodi:nodetypes="sssssssssssccs" /></svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/document_stroke-mod.svg =================================================================== --- trunk/img/icons/custom-iconic/document_stroke-mod.svg (rev 0) +++ trunk/img/icons/custom-iconic/document_stroke-mod.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32px" + height="32.023px" + viewBox="0 0 32 32.023" + style="enable-background:new 0 0 32 32.023;" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="document_stroke-mod.svg"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview5" + showgrid="true" + inkscape:zoom="20.844669" + inkscape:cx="16" + inkscape:cy="16.011499" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid3005" /></sodipodi:namedview> +<path + style="fill:#231f20" + d="M 22.016,0 0,0 -5.9466538e-7,32.023 C 13,32.023 19,32.023 32.000001,32.023 L 32,10.008 z M 28,28.023 c -10.907572,0 -10.783442,0 -24.0000004,0 0,0 0.006,-21.441 0.006,-24.084 2.1,0 7.807,0 12.006,0 l 0,12.01 12.01,0 C 28.021,18.682 28,28.023 28,28.023 z M 20.016,11.945 c 0,-1.543 0,-5.918 0,-8.006 1.615,1.619 6.402,6.404 8.006,8.006 -2.047,0 -6.454,0 -8.006,0 z" + id="path3" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccc" /> +</svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/lists.svg =================================================================== --- trunk/img/icons/custom-iconic/lists.svg (rev 0) +++ trunk/img/icons/custom-iconic/lists.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32px" + height="32px" + viewBox="0 0 32 32" + style="enable-background:new 0 0 32 32;" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="lists.svg"><metadata + id="metadata19"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs17"> + + + + + +</defs><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview15" + showgrid="true" + inkscape:zoom="11.203125" + inkscape:cx="17.097985" + inkscape:cy="13.124115" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid3015" /></sodipodi:namedview> +<g + id="g3000"><polygon + transform="matrix(-1,0,0,1,32,0)" + style="fill:#231f20" + points="32,0 32,4 32,4 0,4 0,0 " + id="polygon5" /><polygon + transform="matrix(-1.4,0,0,1,32,0)" + style="fill:#231f20" + points="20,12 0,12 0,8 20,8 20,12 " + id="polygon7" /><polygon + transform="matrix(-0.875,0,0,1,32,0)" + style="fill:#231f20" + points="32,24 0,24 0,20 32,20 32,24 " + id="polygon9" /><polygon + transform="matrix(-1,0,0,1,32,0)" + style="fill:#231f20" + points="24,28 24,32 24,32 0,32 0,28 " + id="polygon11" /></g> +</svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/location.svg =================================================================== --- trunk/img/icons/custom-iconic/location.svg (rev 0) +++ trunk/img/icons/custom-iconic/location.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32" + height="32" + viewBox="0 0 31.999999 32" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="link.svg"><metadata + id="metadata13"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs11" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview9" + showgrid="true" + inkscape:zoom="22.40625" + inkscape:cx="16" + inkscape:cy="16" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid3009" + empspacing="5" + visible="true" + enabled="true" + snapvisiblegridlinesonly="true" /></sodipodi:namedview> +<g + id="g3" + transform="translate(0,2)"> + <path + style="fill:#231f20" + d="m 16.024,4.007 c 3.32,0 6.019,2.7 6.019,6.017 0,1.894 -0.867,3.636 -2.379,4.773 C 18.042084,16.123195 17,17.96875 16,19.53125 15,17.96875 14,16 12.39,14.797 10.875,13.66 10.008,11.918 10.008,10.024 c 0,-3.317 2.699,-6.017 6.016,-6.017 M 16.024,0 C 10.488,0 6,4.488 6,10.024 c 0,3.28 1.547,6.147 3.977,7.976 1.121,0.844 3.1235,4 3.57325,5 C 14,24 15.331475,26.335992 16,28 c 0.734517,-1.69468 1.812,-4 2.406,-5 0.594,-1 2.547,-4.156 3.668,-5 2.43,-1.829 3.977,-4.696 3.977,-7.976 C 26.051,4.488 21.562,0 16.024,0 z" + id="path5" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cscccsccsczczcsc" /> + +</g> +</svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/picture.svg =================================================================== --- trunk/img/icons/custom-iconic/picture.svg (rev 0) +++ trunk/img/icons/custom-iconic/picture.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32px" + height="32.023px" + viewBox="0 0 32 32.023" + style="enable-background:new 0 0 32 32.023;" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="picture.svg" + inkscape:export-filename="/tmp/picture-black.png" + inkscape:export-xdpi="45" + inkscape:export-ydpi="45"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview5" + showgrid="true" + inkscape:zoom="11.195079" + inkscape:cx="15.028131" + inkscape:cy="14.592141" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid3005" /></sodipodi:namedview> + +<path + style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#231f20;fill-opacity:1;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" + d="M 1.8125 0.03125 A 2.0001999 2.0001999 0 0 0 0 2.03125 L 0 30.03125 A 2.0001999 2.0001999 0 0 0 2 32.03125 L 30 32.03125 A 2.0001999 2.0001999 0 0 0 32 30.03125 L 32 2.03125 A 2.0001999 2.0001999 0 0 0 30 0.03125 L 2 0.03125 A 2.0001999 2.0001999 0 0 0 1.8125 0.03125 z M 4 4.03125 L 28 4.03125 L 28 14.03125 C 28 14.03125 26.974092 14.034149 25.65625 14.0625 C 25.581508 12.992164 25.258639 11.966366 24.71875 11.03125 C 22.797598 7.7037175 18.483783 6.5475981 15.15625 8.46875 C 12.362378 10.081793 11.133571 13.357304 11.9375 16.34375 C 9.6691205 17.30296 7.6457826 18.345626 4 19 L 4 4.03125 z M 18.875 11.5 C 19.832223 11.564262 20.72483 12.121629 21.25 13.03125 C 21.469627 13.411655 21.613234 13.817196 21.65625 14.25 C 19.617583 14.395505 17.468728 14.646833 15.71875 15.0625 C 15.483749 13.844461 16.007763 12.569329 17.15625 11.90625 C 17.702023 11.591148 18.300666 11.461443 18.875 11.5 z M 28 18.03125 L 28 28.03125 L 4 28.03125 L 4 23.03125 A 2.0001999 2.0001999 0 0 0 4.34375 23 C 10.699207 21.940757 12.956538 19.854616 16.5 18.96875 C 19.891283 18.120929 28 18.03125 28 18.03125 z " + id="rect3004" /></svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/reply.svg =================================================================== --- trunk/img/icons/custom-iconic/reply.svg (rev 0) +++ trunk/img/icons/custom-iconic/reply.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32px" + height="31.985px" + viewBox="0 0 32 31.985" + style="enable-background:new 0 0 32 31.985;" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="reply.svg"><metadata + id="metadata13"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs + id="defs11" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview9" + showgrid="false" + inkscape:zoom="20.869432" + inkscape:cx="16" + inkscape:cy="15.9925" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1" /> +<g + id="g3" + transform="matrix(-1,0,0,-1,31.98626,31.985377)"> + <path + style="fill:#231f20" + d="M 28.156,3.72 C 23.191259,-1.3561979 13.995754,-1.2081334 9.2406719,4.087821 7.4768618,5.8148852 5.7546981,7.5837628 4,9.32 4,7.547 4,5.774 4,4.001 c -1.3333333,0 -2.6666667,0 -4,0 0,4 0,8 0,12 4,0 8,0 12,0 0,-1.333333 0,-2.666667 0,-4 -1.675,0 -3.35,0 -5.025,0 2.3669706,-2.2196714 4.439909,-4.7779711 7.05347,-6.7189446 4.151835,-2.59042 10.352865,-1.2404923 12.749396,3.1216783 2.157583,3.6209673 1.362664,8.6925193 -1.835853,11.4513573 -1.001785,0.993873 -3.033417,2.323732 -2.233621,2.558459 4.283435,-0.590885 8.428779,-3.795214 9.173842,-8.201008 C 32.416151,10.382846 30.852862,6.4282488 28.156,3.72 z" + id="path5" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccc" /> + + +</g> +</svg> \ No newline at end of file Added: trunk/img/icons/custom-iconic/wiki_stroke-mod.svg =================================================================== --- trunk/img/icons/custom-iconic/wiki_stroke-mod.svg (rev 0) +++ trunk/img/icons/custom-iconic/wiki_stroke-mod.svg 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + width="32px" + height="32.023px" + viewBox="0 0 32 32.023" + style="enable-background:new 0 0 32 32.023;" + xml:space="preserve" + inkscape:version="0.48.2 r9819" + sodipodi:docname="wiki_stroke-mod.svg"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1078" + inkscape:window-height="919" + id="namedview5" + showgrid="true" + inkscape:zoom="4" + inkscape:cx="16" + inkscape:cy="13.87808" + inkscape:window-x="0" + inkscape:window-y="972" + inkscape:window-maximized="0" + inkscape:current-layer="Layer_1"><inkscape:grid + type="xygrid" + id="grid3005" /></sodipodi:namedview> +<path + style="fill:#231f20" + d="m 23,0.02299881 c 0,0 -12,0 -19.0000004,-0.02299881 C 3.9999996,8.0229991 -5.9466538e-7,32.023 -5.9466538e-7,32.023 13,32.023 21.5,32.023 28,32.023 31.000001,20.023 31.000001,18.022999 31.984,10.030999 28,7.0229991 23,0.02299881 23,0.02299881 z M 25,28.023 c -10.907572,0 -13.911982,0.09595 -20.5202614,0.09595 C 5.9999996,21.023 6.9999997,12.022999 7.4797387,4.022999 c 2.1000003,0 5.3212613,0 9.5202613,0 0,0 0,9 -0.988,11.926001 C 21,16.517683 27,16.022999 27,16.022999 26.999,18.755999 25,28.023 25,28.023 z M 20.431765,12.687182 C 21.415765,8.7651811 21,6.110999 21,4.022999 c 1,2 5.418,6.320001 7.022,7.922001 -3.022,1.077999 -6.038235,0.742182 -7.590235,0.742182 z" + id="path3" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccccccc" /> +</svg> \ No newline at end of file Modified: trunk/pom.xml =================================================================== --- trunk/pom.xml 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/pom.xml 2012-03-08 15:05:45 UTC (rev 1748) @@ -638,6 +638,11 @@ </dependency> <dependency> <groupId>org.waveprotocol</groupId> + <artifactId>client-scheduler</artifactId> + <version>${wiab.version}</version> + </dependency> + <dependency> + <groupId>org.waveprotocol</groupId> <artifactId>client-scheduler-src</artifactId> <version>${wiab.version}</version> </dependency> Modified: trunk/src/main/java/cc/kune/common/client/resources/CommonResources.java =================================================================== --- trunk/src/main/java/cc/kune/common/client/resources/CommonResources.java 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/src/main/java/cc/kune/common/client/resources/CommonResources.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -71,7 +71,34 @@ @Source("cross.png") ImageResource redCross(); + + @Source("icons/chain-close-black.png") + ImageResource chainCloseBlack(); + @Source("icons/chain-close-white.png") + ImageResource chainCloseWhite(); + + @Source("icons/chain-closed-grey.png") + ImageResource chainClosedGrey(); + + @Source("icons/chain-open-black.png") + ImageResource chainOpenBlack(); + + @Source("icons/chain-open-grey.png") + ImageResource chainOpenGrey(); + + @Source("icons/chain-open-white.png") + ImageResource chainOpenWhite(); + + @Source("icons/picture-black.png") + ImageResource pictureBlack(); + + @Source("icons/picture-grey.png") + ImageResource pictureGrey(); + + @Source("icons/picture-white.png") + ImageResource pictureWhite(); + @Source("unchecked.png") ImageResource unChecked(); } Modified: trunk/src/main/java/cc/kune/core/client/resources/CoreResources.java =================================================================== --- trunk/src/main/java/cc/kune/core/client/resources/CoreResources.java 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/src/main/java/cc/kune/core/client/resources/CoreResources.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -19,11 +19,12 @@ */ package cc.kune.core.client.resources; -import com.google.gwt.resources.client.ClientBundle; +import cc.kune.common.client.resources.CommonResources; + import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.ImageResource; -public interface CoreResources extends ClientBundle { +public interface CoreResources extends CommonResources { public interface Style extends CssResource { String loadingSpiner(); } @@ -442,16 +443,16 @@ @Source("person-def.png") ImageResource personDef(); - @Source("pref-black.png") + @Source("icons/pref-black.png") ImageResource prefBlack(); - @Source("pref-grey.png") + @Source("icons/pref-grey.png") ImageResource prefGrey(); @Source("prefs.png") ImageResource prefs(); - @Source("pref-white.png") + @Source("icons/pref-white.png") ImageResource prefWhite(); @Source("question32.png") @@ -547,4 +548,6 @@ @Source("wave-icon.png") ImageResource waveIcon(); + + } Deleted: trunk/src/main/java/cc/kune/core/client/resources/pref-black.png =================================================================== (Binary files differ) Deleted: trunk/src/main/java/cc/kune/core/client/resources/pref-grey.png =================================================================== (Binary files differ) Deleted: trunk/src/main/java/cc/kune/core/client/resources/pref-white.png =================================================================== (Binary files differ) Modified: trunk/src/main/java/cc/kune/events/client/actions/EventAddMenuItem.java =================================================================== --- trunk/src/main/java/cc/kune/events/client/actions/EventAddMenuItem.java 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/src/main/java/cc/kune/events/client/actions/EventAddMenuItem.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -75,7 +75,7 @@ contService.get().addNewContentWithGadgetAndState(session.getUserHash(), session.getContainerState().getStateToken(), EventsConstants.TYPE_MEETING_DEF_GADGETNAME, EventsConstants.TYPE_MEETING, title, - title, EventsClientConversionUtil.toMap(appt), new AsyncCallbackSimple<StateContentDTO>() { + "", EventsClientConversionUtil.toMap(appt), new AsyncCallbackSimple<StateContentDTO>() { @Override public void onFailure(final Throwable caught) { super.onFailure(caught); Modified: trunk/src/main/java/cc/kune/events/client/actions/EventsClientActions.java =================================================================== --- trunk/src/main/java/cc/kune/events/client/actions/EventsClientActions.java 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/src/main/java/cc/kune/events/client/actions/EventsClientActions.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -60,8 +60,8 @@ final Provider<Calendar7DaysViewSelectBtn> cal7DaysBtn, final Provider<CalendarMonthViewSelectBtn> calMonthBtn, final Provider<EventAddMenuItem> eventAddMenuItem, + final Provider<EventOpenMenuItem> eventOpenMenuItem, final Provider<CalendarGoPrevBtn> calPrevBtn, final Provider<EventRemoveMenuItem> eventRemoveMenuItem, - final Provider<EventOpenMenuItem> eventOpenMenuItem, final Provider<CalendarGoPrevBtn> calPrevBtn, final Provider<CalendarGoNextBtn> calNextBtn, final CalendarOnOverMenu onOverMenu, final Provider<CalendarGoTodayBtn> goToday, final Provider<RefreshContentMenuItem> refresh) { super(session, stateManager, i18n, registry); @@ -82,8 +82,8 @@ // On over calendar menu actionsRegistry.addAction(ActionGroups.TOOLBAR, onOverMenu, containers); + eventOpenMenuItem.get(); eventAddMenuItem.get(); - eventOpenMenuItem.get(); eventRemoveMenuItem.get(); // For now, commented: Modified: trunk/src/main/java/cc/kune/events/client/viewer/CalendarViewerPanel.java =================================================================== --- trunk/src/main/java/cc/kune/events/client/viewer/CalendarViewerPanel.java 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/src/main/java/cc/kune/events/client/viewer/CalendarViewerPanel.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -22,6 +22,7 @@ import com.bradrydzewski.gwt.calendar.client.Appointment; import com.bradrydzewski.gwt.calendar.client.Calendar; import com.bradrydzewski.gwt.calendar.client.CalendarSettings; +import com.bradrydzewski.gwt.calendar.client.CalendarSettings.Click; import com.bradrydzewski.gwt.calendar.client.CalendarViews; import com.bradrydzewski.gwt.calendar.client.event.CreateHandler; import com.bradrydzewski.gwt.calendar.client.event.DateRequestHandler; @@ -65,6 +66,7 @@ final CalendarSettings settings = new CalendarSettings(); settings.setIntervalsPerHour(4); settings.setPixelsPerInterval(10); + settings.setTimeBlockClickNumber(Click.Single); // settings.setOffsetHourLabels(true); settings.setScrollToHour(8); calendar = new Calendar(); Modified: trunk/src/main/java/cc/kune/events/shared/EventsClientConversionUtil.java =================================================================== --- trunk/src/main/java/cc/kune/events/shared/EventsClientConversionUtil.java 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/src/main/java/cc/kune/events/shared/EventsClientConversionUtil.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -23,6 +23,9 @@ if (end != null) { app.setEnd(DateUtils.toDate(end)); } + String allDay = properties.get(ICalConstants._ALL_DAY); + if (allDay != null) + app.setAllDay(Boolean.parseBoolean(allDay)); return app; } @@ -30,6 +33,7 @@ final Map<String, String> properties = EventsSharedConversionUtil.toMap(app); properties.put(ICalConstants.DATE_TIME_START, DateUtils.toString(app.getStart())); properties.put(ICalConstants.DATE_TIME_END, DateUtils.toString(app.getEnd())); + properties.put(ICalConstants._ALL_DAY, Boolean.toString(app.isAllDay())); return properties; } Added: trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/Gadget.css =================================================================== --- trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/Gadget.css (rev 0) +++ trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/Gadget.css 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,136 @@ +/* + * StyleInjector CSS file for Gadget container. + * Author: [email protected] (Vadim Gerasimov) + */ + +.panel { + position: relative; /* Positioned container */ + zoom: 1; /* Fixes IE positioning bug. */ + overflow: hidden; /* Expand to size of floated contents. */ + font-size: 0pt; + border: none; + clear: left; +} + +.inline { + display: inline-block; + vertical-align: bottom; + width: 100%; +} + +.title { + display: none; +} + +.gadgetFrame { + margin-top: 0px; + margin-bottom: 0px; + font-size: 0pt; + display: block; + position: relative; +} + +.loadingGadgetFrame { + border: 1px solid #b8c6d9; +} + +.loadedGadgetFrame { + border: 1px solid transparent; +} + +@sprite .loadingGadgetLargeThrobber { + gwt-image: 'loadingGadgetLarge'; + background-color: #e6ebf2; + background-repeat: no-repeat; + line-height: value('loadingGadgetLarge.getHeight', 'px'); + background-position: center center; + display: block; +} + +@sprite .loadingGadgetSmallThrobber { + gwt-image: 'loadingGadgetSmall'; + background-color: #e6ebf2; + background-repeat: no-repeat; + line-height: value('loadingGadgetSmall.getHeight', 'px'); + background-position: center center; + display: block; +} + +@url brokenGadgetUrl brokenGadget; +.brokenGadgetIcon { + background-color: #e6ebf2; + background-image: brokenGadgetUrl; + background-repeat: no-repeat; + background-position: center center; + display: block; +} + +.loadedGadget { + display: block; +} + +.iframeDiv { + position: relative; + left: 0px; + top: 0px; +} + +/* Buttons on each widget. */ +@eval metaButtonTransitionTime RESOURCES.FADE_DELAY_STRING; +@sprite .metaButtons { + position: absolute; + right: 14px; + top: 6px; + z-index: 1; + gwt-image: 'metaMid'; + overflow: visible; /* GWT's sprite rules stick in an overflow:hidden which we don't want. */ + font-size: 0pt; + + /** animate fade-in and fade-out of the meta buttons */ + \-webkit-transition: opacity metaButtonTransitionTime ease-in-out; + \-moz-transition: opacity metaButtonTransitionTime ease-in-out; + transition: opacity metaButtonTransitionTime ease-in-out; +} + +/* + * The left and right + */ + +@eval metaLeftMargin RESOURCES.META_LEFT_WIDTH; +@eval metaRightMargin RESOURCES.META_RIGHT_WIDTH; + +@sprite .metaRight { + gwt-image: 'metaRightImage'; + float: left; + margin-right: metaRightMargin; +} + +@sprite .metaLeft { + gwt-image: 'metaLeftImage'; + float: left; + margin-left: metaLeftMargin; +} + +.metaButtonsPanel { + float: left; +} + +.metaButton { + /* Push the buttons down to align nicely with the mid, right and left background images. */ + margin-top: 3px; + float: left; +} + +.metaButton.disabled { + opacity: 0.5; +} + +@if user.agent ie6 { + .metaButton.disabled { + filter: "alpha(opacity = 50)"; + } +} + +@sprite .more { + gwt-image: 'moreImage'; +} Added: trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidget.java =================================================================== --- trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidget.java (rev 0) +++ trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidget.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,2036 @@ +// @formatter:off +/** + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.waveprotocol.wave.client.gadget.renderer; + +import static org.waveprotocol.wave.model.gadget.GadgetConstants.AUTHOR_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.ID_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.IFRAME_URL_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.LAST_KNOWN_HEIGHT_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.LAST_KNOWN_WIDTH_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.PREFS_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.SNIPPET_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.STATE_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.TITLE_ATTRIBUTE; +import static org.waveprotocol.wave.model.gadget.GadgetConstants.URL_ATTRIBUTE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.ScriptElement; +import com.google.gwt.http.client.URL; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Random; +import com.google.gwt.user.client.Window.Location; + +import org.waveprotocol.wave.client.account.ProfileManager; +import org.waveprotocol.wave.client.common.util.UserAgent; +import org.waveprotocol.wave.client.editor.content.AnnotationPainter; +import org.waveprotocol.wave.client.editor.content.CMutableDocument; +import org.waveprotocol.wave.client.editor.content.ContentElement; +import org.waveprotocol.wave.client.editor.content.ContentNode; +import org.waveprotocol.wave.client.gadget.GadgetLog; +import org.waveprotocol.wave.client.gadget.StateMap; +import org.waveprotocol.wave.client.gadget.StateMap.Each; +import org.waveprotocol.wave.client.scheduler.ScheduleCommand; +import org.waveprotocol.wave.client.scheduler.ScheduleTimer; +import org.waveprotocol.wave.client.scheduler.Scheduler; +import org.waveprotocol.wave.client.scheduler.Scheduler.Task; +import org.waveprotocol.wave.model.conversation.ConversationBlip; +import org.waveprotocol.wave.model.conversation.ObservableConversation; +import org.waveprotocol.wave.model.document.util.Point; +import org.waveprotocol.wave.model.document.util.XmlStringBuilder; +import org.waveprotocol.wave.model.gadget.GadgetXmlUtil; +import org.waveprotocol.wave.model.id.ModernIdSerialiser; +import org.waveprotocol.wave.model.id.WaveletName; +import org.waveprotocol.wave.model.supplement.ObservableSupplementedWave; +import org.waveprotocol.wave.model.util.CollectionUtils; +import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV; +import org.waveprotocol.wave.model.util.ReadableStringSet; +import org.waveprotocol.wave.model.util.StringMap; +import org.waveprotocol.wave.model.wave.ParticipantId; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * Class to implement gadget widgets rendered in the client. + * + * + * TODO(user): Modularize the gadget APIs (base, Podium, Wave, etc). + * + * TODO(user): Refactor the common RPC call code. + */ +public class GadgetWidget extends ObservableSupplementedWave.ListenerImpl + implements GadgetRpcListener, GadgetWaveletListener, GadgetUiListener { + + private static final String GADGET_RELAY_PATH = "gadgets/files/container/rpc_relay.html"; + private static final int DEFAULT_HEIGHT_PX = 100; + private static final int DEFAULT_WIDTH_PX = -1; + + /** + * Helper class to analyze element changes in the gadget state and prefs. + */ + private abstract class ElementChangeTask { + /** + * Runs processChange() wrapped in code that detects and submits changes in + * the gadget state and prefs. + * + * @param node The node being processed or null if not defined. + */ + void run(ContentNode node) { + if (!isActive()) { + log("Element change event in removed node: ignoring."); + return; + } + StateMap oldState = StateMap.create(); + oldState.copyFrom(state); + final StateMap oldPrefs = StateMap.create(); + oldPrefs.copyFrom(userPrefs); + processChange(node); + if (!state.compare(oldState)) { + gadgetStateSubmitter.submit(); + } + // TODO(user): Optimize prefs updates. + if (!userPrefs.compare(oldPrefs)) { + userPrefs.each(new StateMap.Each() { + @Override + public void apply(String key, String value) { + if (!oldPrefs.has(key) || !value.equals(oldPrefs.get(key))) { + setGadgetPref(key, value); + } + } + }); + } + } + + /** + * Processes the changes in the elements. + * + * @param node The node being processed or null if not defined. + */ + abstract void processChange(ContentNode node); + } + + /** + * Podium state is stored as a part of the wave gadget state and can be + * visible to the Gadget via both Wave and Podium RPC interfaces. + */ + private static final String PODIUM_STATE_NAME = "podiumState"; + + /** + * Gadget RPC path: location of the RPC JavaScript code to be loaded into the + * client code. This is the standard Gadget library to support RPCs. + */ + static final String GADGET_RPC_PATH = "/gadgets/js/core:rpc.js"; + + /** + * Gadget name prefix: the common part of the gadget IFrame ID and name. The + * numeric gadget ID is appended to this prefix. + */ + static final String GADGET_NAME_PREFIX = "wgadget_iframe_"; + + /** Primary view for gadgets. */ + static final String GADGET_PRIMARY_VIEW = "canvas"; + + /** Default view for gadgets. */ + static final String GADGET_DEFAULT_VIEW = "default"; + + /** + * Time in milliseconds to wait for the RPC script to load before logging a + * warning. + */ + private static final int GADGET_RPC_LOAD_WARNING_TIMEOUT_MS = 30000; + + /** Time granularity to check for the Gadget RPC library load state. */ + private static final int GADGET_RPC_LOAD_TIMER_MS = 250; + + /** Editing mode polling timer. */ + private static final int EDITING_POLLING_TIMER_MS = 200; + + /** Blip submit delay in milliseconds. */ + private static final int BLIP_SUBMIT_TIMEOUT_MS = 30; + + /** Gadget state send delay in milliseconds. */ + private static final int STATE_SEND_TIMEOUT_MS = 30; + + /** The Wave API version supported by the gadget container. */ + private static final String WAVE_API_VERSION = "1"; + + /** The key for the playback state in the wave gadget state map. */ + private static final String PLAYBACK_MODE_KEY = "${playback}"; + + /** The key for the edit state in the wave gadget state map. */ + private static final String EDIT_MODE_KEY = "${edit}"; + + /** Gadget-loading frame border removal delay in ms. */ + private static final int FRAME_BORDER_REMOVE_DELAY_MS = 3000; + + /** Delay before sending one more participant information update in ms. */ + private static final int REPEAT_PARTICIPANT_INFORMATION_SEND_DELAY_MS = 5000; + + /** Object that manages Gadget UI HTML elements. */ + private GadgetWidgetUi ui; + + /** Gadget title element. */ + private GadgetElementChild titleElement; + + /** The gadget spec URL. */ + private String source; + + /** Gadget instance ID counter (local for each client). */ + private static int nextClientInstanceId = 0; + + /** Gadget instance ID. Non-final for testing. */ + private int clientInstanceId; + + /** Gadget iframe URL. */ + private String iframeUrl; + + /** Gadget RPC token.*/ + private final String rpcToken; + + /** Gadget security token. */ + private String securityToken; + + /** Gadget user preferences. */ + private GadgetUserPrefs userPrefs; + + /** + * Gadget state element map. Maps state keys to the corresponding elements. + */ + private final StringMap<GadgetElementChild> prefElements; + + /** + * Widget active flag: true after the widget is created, false after it is + * destroyed. + */ + private boolean active = false; + + /** ID of the gadget's wave/let. */ + private WaveletName waveletName; + + /** Host blip of this gadget. */ + private ConversationBlip blip; + + /** Blip submitter. */ + private Submitter blipSubmitter; + + /** Gadget state submitter. */ + private Submitter gadgetStateSubmitter; + + /** Private gadget state submitter. */ + private Submitter privateGadgetStateSubmitter; + + /** ContentElement in the wave that corresponds to this gadget. */ + private ContentElement element; + + /** Indicator for gadget's blip editing state. */ + private EditingIndicator editingIndicator; + + /** Participant information. */ + private ParticipantInformation participants; + + /** Gadget state. */ + private StateMap state; + + /** User id of the current logged in user. */ + private String loginName; + + /** + * Gadget state element map. Maps state keys to the corresponding elements. + */ + private final StringMap<GadgetElementChild> stateElements; + + /** Indicates whether the gadget is known to support the Wave API. */ + private boolean waveEnabled = false; + + /** Version of Wave API that is used by the gadget-side code. */ + private String waveApiVersion = ""; + + /** Per-user wavelet to store private gadget data. */ + private ObservableSupplementedWave supplement; + + /** Provides profile information. */ + private ProfileManager profileManager; + + /** Wave client locale. */ + private Locale locale; + + /** Gadget library initialization flag. */ + private static boolean initialized = false; + + /** + * Gadget element child that defines what nodes to check for redundancy in the + * removeRedundantNodeTask. Only a single task can be scheduled at a time. + */ + private GadgetElementChild redundantNodeCheckChild = null; + + /** + * Indicates whether the gadget has performed a document mutation on behalf of + * the user. This flag is checked when the gadget tries to perform + * non-essential modifications of the document such as duplicate node cleanup + * or height attribute update. Performing such operations may generate + * unnecessary playback frames and attribute modifications to a user who did + * not use the gadget. The flag is set when the gadget modifies state, prefs, + * title, or any other elements that normally are linked to user actions in + * the gadget. + */ + private boolean documentModified = false; + + /** + * Indicates that the iframe URL attribute should be updated when the gadget + * modifies the document in response to a user action. + */ + private boolean toUpdateIframeUrl = false; + + private final String clientInstanceLogLabel; + + // Note that the following regex expressions are strings rather than compiled patterns because GWT + // does not (yet) support those. Consider using the new GWT RegExp class in the future. + + /** + * Pattern to match rpc token, security token, and user preference parameters + * in a URL fragment. Used to remove all these parameters. + */ + private final static String FRAGMENT_CLEANING_PATTERN = "(^|&)(rpctoken=|st=|up_)[^&]*"; + + /** + * Pattern to match module ID and security token parameters a URL. Used to + * remove all these parameters. + */ + private final static String URL_CLEANING_PATTERN = "&(mid=|st=|lang=|country=|debug=)[^&]*"; + + /** + * Pattern to match and remove URL fragment including the #. + */ + private final static String FRAGMENT_PATTERN = "#.*"; + + /** + * Pattern to match and remove URL part before fragment including the #. + */ + private final static String BEFORE_FRAGMENT_PATTERN = "[^#]*#"; + + /** + * Pattern to validate URL fragment. + */ + private final static String FRAGMENT_VALIDATION_PATTERN = + "([\\w~!&@\\$\\-\\.\\'\\(\\)\\*\\+\\,\\;\\=\\?\\:]|%[0-9a-fA-F]{2})+"; + + /** + * Pattern to match iframe host in the beginning of a URL. This is not a + * validation check. The user can choose their own host. This simply serves + * to extract the iframe segment of the URL + */ + private final static String IFRAME_HOST_PATTERN = + "^\\/\\/(https?:\\/\\/)?[^\\/]+\\/"; + + /** + * Pattern to remove XML-unsafe characters. Snippeting fails on some of those + * symbol combinations due to a potential bug in XML attribute processing. + * Theoretically all those symbols should be tolerated and displayed in + * snippets without any special processing in this class. + * + * TODO(user): Investigate/test this later to remove sanitization. + */ + private final static String SNIPPET_SANITIZER_PATTERN = "[<>\\\"\\'\\&]"; + + + /** + * Constructs GadgetWidget for testing. + */ + private GadgetWidget() { + clientInstanceId = nextClientInstanceId++; + clientInstanceLogLabel = "[" + clientInstanceId + "]"; + prefElements = CollectionUtils.createStringMap(); + stateElements = CollectionUtils.createStringMap(); + rpcToken = "" + + ((Long.valueOf(Random.nextInt()) << 32) | (Long.valueOf(Random.nextInt()) & 0xFFFFFFFFL)); + } + + private static native boolean gadgetLibraryLoaded() /*-{ + return ($wnd.gadgets && $wnd.gadgets.rpc) ? true : false; + }-*/; + + /** + * Preloads the libraries and initializes them on the first use. + */ + private static void initializeGadgets() { + if (!initialized && !gadgetLibraryLoaded()) { + GadgetLog.log("Initializing Gadget RPC script tag."); + loadGadgetRpcScript(); + initialized = true; + GadgetLog.log("Gadgets RPC script tag initialized."); + } + // TODO(user): Remove the css hacks once CAJA is fixed. + if (!initialized && !gadgetLibraryLoaded()) { + // HACK(user): NOT reachable, but GWT thinks it is. + excludeCssName(); + } + } + + /** + * Utility function to convert a Gadget StateMap to a string to be stored as + * an attribute value. + * + * @param state JSON object to be converted to string. + * @return string to be saved as an attribute value. + */ + private static String stateToAttribute(StateMap state) { + if (state == null) { + return URL.encodeComponent("{}"); + } + return URL.encodeComponent(state.toJson()); + } + + /** + * Utility function to convert an attribute string to a Gadget StateMap. + * + * @param attribute attribute value string. + * @return StateMap constructed from the attribute value. + */ + private StateMap attributeToState(String attribute) { + StateMap result = StateMap.create(); + if ((attribute != null) && !attribute.equals("")) { + log("Unescaped attribute: ", URL.decodeComponent(attribute)); + result.fromJson(URL.decodeComponent(attribute)); + log("State map: ", result.toJson()); + } + return result; + } + + /** + * Returns the gadget name that identifies the gadget and its frame. + * + * @return gadget name. + */ + private String getGadgetName() { + return GADGET_NAME_PREFIX + clientInstanceId; + } + + private void updatePrefsFromAttribute(String prefAttribute) { + if (!stateToAttribute(userPrefs).equals(prefAttribute)) { + StateMap prefState = attributeToState(prefAttribute); + userPrefs.parse(prefState, true); + log("Updating user prefs: ", userPrefs.toJson()); + prefState.each(new StateMap.Each() { + @Override + public void apply(String key, String value) { + setGadgetPref(key, value); + } + }); + } + } + + /** + * Processes changes in the gadget element attributes. + * TODO(user): move some of this code to the handler. + * + * @param name attribute name. + * @param value new attribute value. + */ + public void onAttributeModified(String name, String value) { + log("Attribute '", name, "' changed to '", value, "'"); + if (userPrefs == null) { + log("Attribute changed before the gadget is initialized."); + return; + } + + if (name.equals(URL_ATTRIBUTE)) { + source = (value == null) ? "" : value; + } else if (name.equals(TITLE_ATTRIBUTE)) { + String title = (value == null) ? "" : URL.decodeComponent(value); + if (!title.equals(ui.getTitleLabelText())) { + log("Updating title: ", title); + ui.setTitleLabelText(title); + } + } else if (name.equals(PREFS_ATTRIBUTE)) { + updatePrefsFromAttribute(value); + } else if (name.equals(STATE_ATTRIBUTE)) { + StateMap newState = attributeToState(value); + if (!state.compare(newState)) { + String podiumState = newState.get(PODIUM_STATE_NAME); + if ((podiumState != null) && (!podiumState.equals(state.get(PODIUM_STATE_NAME)))) { + sendPodiumOnStateChangedRpc(getGadgetName(), podiumState); + } + state.clear(); + state.copyFrom(newState); + log("Updating gadget state: ", state.toJson()); + gadgetStateSubmitter.submit(); + } + } + } + + /** + * Loads Gadget RPC library script. + */ + private static void loadGadgetRpcScript() { + ScriptElement script = Document.get().createScriptElement(); + script.setType("text/javascript"); + script.setSrc(GADGET_RPC_PATH); + Document.get().getBody().appendChild(script); + } + + /** + * Appends tokens to the iframe URI fragment. + * + * @param fragment Original parameter fragment of the gadget URI. + * @return Updated parameter fragment with new RPC and security tokens. + */ + private String updateGadgetUriFragment(String fragment) { + fragment = "rpctoken=" + rpcToken + + (fragment.isEmpty() || (fragment.charAt(0) == '&') ? "" : "&") + fragment; + if ((securityToken != null) && !securityToken.isEmpty()) { + fragment += "&st=" + URL.encodeComponent(securityToken); + } + return fragment; + } + + @VisibleForTesting + static String cleanUrl(String url) { + String baseUrl = url; + String fragment = ""; + int fragmentIndex = url.indexOf("#"); + if (fragmentIndex >= 0) { + fragment = (url.substring(fragmentIndex + 1)).replaceAll(FRAGMENT_CLEANING_PATTERN, ""); + if (fragment.startsWith("&")) { + fragment = fragment.substring(1); + } + baseUrl = url.substring(0, fragmentIndex); + } + baseUrl = baseUrl.replaceAll(URL_CLEANING_PATTERN, ""); + return baseUrl + (fragment.isEmpty() ? "" : "#" + fragment); + } + + /** + * Constructs IFrame URI of this gadget. + * + * @param instanceId instance to encode in the URI. + * @param url URL template. + * @return IFrame URI of this gadget. + */ + String buildIframeUrl(int instanceId, String url) { + final StringBuilder builder = new StringBuilder(); + String fragment = ""; + int fragmentIndex = url.indexOf("#"); + if (fragmentIndex >= 0) { + fragment = url.substring(fragmentIndex + 1); + url = url.substring(0, fragmentIndex); + } + builder.append(url); + + boolean enableGadgetCache = false; + + builder.append("&nocache=" + (enableGadgetCache ? "0" : "1")); + builder.append("&mid=" + instanceId); + builder.append("&lang=" + locale.getLanguage()); + builder.append("&country=" + locale.getCountry()); + String href = getUrlPrefix(); + // TODO(user): Parent is normally the last non-hash parameter. It is moved + // as a temp fix for kitchensinky. Move it back when the kitchensinky is + // working wihout this workaround. + builder.append("&parent=" + URL.encode(href)); + builder.append("&wave=" + WAVE_API_VERSION); + builder.append("&waveId=" + URL.encodeQueryString( + ModernIdSerialiser.INSTANCE.serialiseWaveId(waveletName.waveId))); + fragment = updateGadgetUriFragment(fragment); + if (!fragment.isEmpty()) { + builder.append("#" + fragment); + log("Appended fragment: ", fragment); + } + if (userPrefs != null) { + userPrefs.each(new StateMap.Each() { + @Override + public void apply(String key, String value) { + if (value != null) { + builder.append("&up_"); + builder.append(URL.encodeQueryString(key)); + builder.append('='); + builder.append(URL.encodeQueryString(value)); + } + } + }); + } + return builder.toString(); + } + + /** + * Verifies that the gadget has non-empty attribute. + * + * @param name attribute name. + * @return true if non-empty height attribute exists, flase otherwise. + */ + private boolean hasAttribute(String name) { + if (element.hasAttribute(name)) { + String value = element.getAttribute(name); + if (!"".equals(value)) { + return true; + } + } + return false; + } + + /** + * Updates the gadget attribute in a deferred command if the panel is + * editable. + * + * @param attributeName attribute name. + * @param value new attribute value. + */ + private void scheduleGadgetAttributeUpdate(final String attributeName, final String value) { + ScheduleCommand.addCommand(new Scheduler.Task() { + @Override + public void execute() { + if (canModifyDocument() && documentModified) { + String oldValue = element.getAttribute(attributeName); + if (!value.equals(oldValue)) { + element.getMutableDoc().setElementAttribute(element, attributeName, value); + } + } + } + }); + } + + /** + * Updates gadget IFrame attributes. + * + * @param url URL template for the iframe. + * @param width preferred width of the iframe. + * @param height preferred height of the iframe. + */ + private void updateGadgetIframe(String url, long width, long height) { + if (!isActive()) { + return; + } + iframeUrl = url; + if (hasAttribute(LAST_KNOWN_WIDTH_ATTRIBUTE)) { + setSavedIframeWidth(); + } else if (width != 0) { + ui.setIframeWidth(width + "px"); + ui.makeInline(); + scheduleGadgetAttributeUpdate(LAST_KNOWN_WIDTH_ATTRIBUTE, Long.toString(width)); + } else { + ui.setIframeWidth("100%"); + } + if (!hasAttribute(LAST_KNOWN_HEIGHT_ATTRIBUTE) && (height != 0)) { + ui.setIframeHeight(height); + scheduleGadgetAttributeUpdate(LAST_KNOWN_HEIGHT_ATTRIBUTE, Long.toString(height)); + } + String ifr = buildIframeUrl(getInstanceId(), url); + log("ifr: ", ifr); + ui.setIframeSource(ifr); + } + + private int parseSizeString(String heightString) throws NumberFormatException { + if (heightString.endsWith("px")) { + return Integer.parseInt(heightString.substring(0, heightString.length() - 2)); + } else { + return Integer.parseInt(heightString); + } + } + + /** + * Updates gadget iframe height if the gadget has the height attribute. + */ + private void setSavedIframeHeight() { + if (hasAttribute(LAST_KNOWN_HEIGHT_ATTRIBUTE)) { + String savedHeight = element.getAttribute(LAST_KNOWN_HEIGHT_ATTRIBUTE); + try { + int height = parseSizeString(savedHeight); + ui.setIframeHeight(height); + } catch (NumberFormatException e) { + log("Invalid saved height attribute (ignored): ", savedHeight); + } + } + } + + /** + * Updates gadget iframe height if the gadget has the height attribute. + */ + private void setSavedIframeWidth() { + if (hasAttribute(LAST_KNOWN_WIDTH_ATTRIBUTE)) { + String savedWidth = element.getAttribute(LAST_KNOWN_WIDTH_ATTRIBUTE); + try { + int width = parseSizeString(savedWidth); + ui.setIframeWidth(width + "px"); + ui.makeInline(); + } catch (NumberFormatException e) { + log("Invalid saved width attribute (ignored): ", savedWidth); + } + } + } + + /** + * Creates a display widget for the gadget. + * + * @param element ContentElement from the wave. + * @param blip gadget blip. + * @return display widget for the gadget. + */ + public static GadgetWidget createGadgetWidget(ContentElement element, WaveletName waveletName, + ConversationBlip blip, ObservableSupplementedWave supplement, + ProfileManager profileManager, Locale locale, String loginName) { + + final GadgetWidget widget = GWT.create(GadgetWidget.class); + + widget.element = element; + widget.editingIndicator = + new BlipEditingIndicator(element.getRenderedContentView().getDocumentElement()); + widget.ui = new GadgetWidgetUi(widget.getGadgetName(), widget.editingIndicator); + widget.state = StateMap.create(); + initializeGadgets(); + widget.blip = blip; + widget.initializeGadgetContainer(); + widget.ui.setGadgetUiListener(widget); + widget.waveletName = waveletName; + widget.supplement = supplement; + widget.profileManager = profileManager; + widget.locale = locale; + widget.loginName = loginName; + supplement.addListener(widget); + return widget; + } + + /** + * @return the actual GWT widget + */ + public GadgetWidgetUi getWidget() { + return ui; + } + + @Override + public void setTitle(String title) { + if (!isActive()) { + return; + } + final String newTitle = (title == null) ? "" : title; + log("Set title '", XmlStringBuilder.createText(newTitle), "'"); + if (titleElement == null) { + onModifyingDocument(); + GadgetElementChild.create(element.getMutableDoc().insertXml( + Point.end((ContentNode) element), GadgetXmlUtil.constructTitleXml(newTitle))); + blipSubmitter.submit(); + } else { + if (!title.equals(titleElement.getValue())) { + onModifyingDocument(); + titleElement.setValue(newTitle); + blipSubmitter.submit(); + } + } + } + + @Override + public void logMessage(String message) { + GadgetLog.developerLog(message); + } + + private String sanitizeSnippet(String snippet) { + return snippet.replaceAll(SNIPPET_SANITIZER_PATTERN, " "); + } + + @Override + public void setSnippet(String snippet) { + if (!canModifyDocument()) { + return; + } + String safeSnippet = sanitizeSnippet(snippet); + log("Snippet changed: " + safeSnippet); + scheduleGadgetAttributeUpdate(SNIPPET_ATTRIBUTE, safeSnippet); + } + + /** + * Gets the attribute value from the mutable document associated with the + * gadget. + * + * @param attributeName name of the attribute + * @return attribute value or empty string if attribute is missing + */ + private String getAttribute(String attributeName) { + return element.hasAttribute(attributeName) ? element.getAttribute(attributeName) : ""; + } + + @VisibleForTesting + static String getIframeHost(String url) { + // Ideally this should be done with regex matcher which is not supported in GWT. + String iframeHostMatcher = url.replaceFirst(IFRAME_HOST_PATTERN, ""); + if (iframeHostMatcher.length() != url.length()) { + return url.substring(0, url.length() - iframeHostMatcher.length()); + } else { + return ""; + } + } + + /** + * Controller registration task. + * + * @param url URL template of the gadget iframe. + * @param width preferred iframe width. + * @param height preferred iframe height. + */ + private void controllerRegistration(String url, long width, long height) { + Controller controller = Controller.getInstance(); + String iframeHost = getIframeHost(url); + String relayUrl = iframeHost + GADGET_RELAY_PATH; + controller.setRelayUrl(getGadgetName(), relayUrl); + controller.registerGadgetListener(getGadgetName(), GadgetWidget.this); + controller.setRpcToken(getGadgetName(), rpcToken); + updateGadgetIframe(url, width, height); + removeFrameBorder(); + + delayedPodiumInitialization(); + log("Gadget ", getGadgetName(), " is registered, relayUrl=", relayUrl, + ", RPC token=", rpcToken); + } + + private void registerWithController(String url, long width, long height) { + if (gadgetLibraryLoaded()) { + controllerRegistration(url, width, height); + } else { + scheduleControllerRegistration(url, width, height); + } + } + + /** + * Registers the Gadget object as RPC event listener with the Gadget RPC + * Controller after waiting for the Gadget RPC library to load. + */ + private void scheduleControllerRegistration( + final String url, final long width, final long height) { + new ScheduleTimer() { + private double loadWarningTime = + Duration.currentTimeMillis() + GADGET_RPC_LOAD_WARNING_TIMEOUT_MS; + @Override + public void run() { + if (!isActive()) { + cancel(); + log("Not active."); + return; + } else if (gadgetLibraryLoaded()) { + cancel(); + controllerRegistration(url, width, height); + } else { + if (Duration.currentTimeMillis() > loadWarningTime) { + log("Gadget RPC script failed to load on time."); + loadWarningTime += GADGET_RPC_LOAD_WARNING_TIMEOUT_MS; + } + } + } + }.scheduleRepeating(GADGET_RPC_LOAD_TIMER_MS); + } + + private void initializeGadgetContainer() { + userPrefs = GadgetUserPrefs.create(); + blipSubmitter = new Submitter(BLIP_SUBMIT_TIMEOUT_MS, new Submitter.SubmitTask() { + @Override public void doSubmit() { + // TODO: send a playback frame signal. + log("Blip submitted."); + } + }); + gadgetStateSubmitter = new Submitter(STATE_SEND_TIMEOUT_MS, new Submitter.SubmitTask() { + @Override public void doSubmit() { + sendGadgetState(); + log("Gadget state sent."); + } + }); + privateGadgetStateSubmitter = new Submitter(STATE_SEND_TIMEOUT_MS, new Submitter.SubmitTask() { + @Override public void doSubmit() { + sendPrivateGadgetState(); + log("Private gadget state sent."); + } + }); + } + + private void initializePodium() { + if (!isActive()) { + // If the widget does not exist, exit. + return; + } + for (ParticipantId participant : blip.getConversation().getParticipantIds()) { + String myId = participants.getMyId(); + if ((myId != null) && !participant.getAddress().equals(myId)) { + String opponentId = participant.getAddress(); + try { + sendPodiumOnInitializedRpc(getGadgetName(), myId, opponentId); + log("Sent Podium initialization: " + myId + " " + opponentId); + String podiumState = state.get(PODIUM_STATE_NAME); + if (podiumState != null) { + sendPodiumOnStateChangedRpc(getGadgetName(), podiumState); + log("Sent Podium state update."); + } + } catch (Exception e) { + // This is a catch to avoid sending RPCs to deleted gadgets. + log("Podium initialization failure"); + } + return; + } + } + log("Podium is not initialized: less than two participants."); + } + + private void delayedPodiumInitialization() { + // TODO(user): This is a hack to delay Podium initialization. + // Define an initialization protocol for Podium to avoid this. + new ScheduleTimer() { + @Override + public void run() { + initializePodium(); + } + }.schedule(3000); + } + + private void removeFrameBorder() { + new ScheduleTimer() { + @Override + public void run() { + ui.removeThrobber(); + } + }.schedule(FRAME_BORDER_REMOVE_DELAY_MS); + } + + private void constructGadgetFromMetadata(GadgetMetadata metadata, String view, String token) { + log("Received metadata: ", metadata.getIframeUrl(view)); + String url = cleanUrl(metadata.getIframeUrl(view)); + if (url.equals(iframeUrl) && ((token == null) || token.isEmpty())) { + log("Received metadata matches the cached information."); + constructGadgetSizeFromMetadata(metadata, view, url); + return; + } + // NOTE(user): Technically we should not save iframe URLs for gadgets with security tokens, + // but some gadgets, such as YNM, that depend on opensocial libraries get security tokens they + // never use. Also to enable gadgets in Ripple and other light Wave clients it's desirable to + // to always have the iframe URL at least for rudimentary rendering. + if (canModifyDocument() && documentModified) { + scheduleGadgetAttributeUpdate(IFRAME_URL_ATTRIBUTE, url); + } else { + toUpdateIframeUrl = true; + } + securityToken = token; + if ("".equals(ui.getTitleLabelText()) && metadata.hasTitle()) { + ui.setTitleLabelText(metadata.getTitle()); + } + constructGadgetSizeFromMetadata(metadata, view, url); + } + + private void constructGadgetSizeFromMetadata(GadgetMetadata metadata, String view, String url) { + int height = + (int) (metadata.hasHeight() ? metadata.getHeight() : metadata.getPreferredHeight(view)); + int width = + (int) (metadata.hasWidth() ? metadata.getWidth() : metadata.getPreferredWidth(view)); + registerWithController(url, width, height); + if (height > 0) { + setIframeHeight(String.valueOf(height)); + } else { + setIframeHeight(String.valueOf(DEFAULT_HEIGHT_PX)); + } + if (width > 0){ + setIframeWidth(String.valueOf(width)); + } else { + setIframeWidth(String.valueOf(DEFAULT_WIDTH_PX)); + } + } + + /** + * This function generates a gadget instance ID for generating gadget metadata + * and security tokens. The ID should be 1. hard to guess; 2. same for the + * same gadget element for the same participant in the same wave every time + * the wave is rendered in the same client; 3. preferably, but not necessarily + * different for different gadget elements and different participants. + * + * Condition 2 is needed to achieve consistent behavior in gadgets that, for + * example, request special permissions using OAuth/OpenSocial. + * + * This function satisfies those conditions, except the ID is going to be + * always the same for the same type of the gadget in the same wavelet for the + * same participant. This poses minimal risk (in terms of matching domains and + * security tokens) because the gadgets with matching IDs would be rendered + * for the same person in the same wave. + * + * NOTE(user): Instance ID should be non-negative number to work around a + * bug in GGS and/or Linux libraries that produces non-renderable iframe URLs + * for negative instance IDs. The domain name starts with dash "-". Browsers + * in Windows and Mac OS tolerate this, but browsers in Linux fail to render + * such URLs. + * + * @return instance ID for the gadget. + */ + private int getInstanceId() { + String name = ModernIdSerialiser.INSTANCE.serialiseWaveletName(waveletName); + String instanceDescriptor = name + loginName + source; + int hash = instanceDescriptor.hashCode(); + return (hash < 0) ? ~hash : hash; + } + + private void showBrokenGadget(String message) { + ui.showBrokenGadget(message); + log("Broken gadget: ", message); + } + + private boolean validIframeUrl(String url) { + return (url != null) && !url.isEmpty() && !getIframeHost(url).isEmpty(); + } + + private void scheduleGadgetIdUpdate() { + ScheduleCommand.addCommand(new Scheduler.Task() { + @Override + public void execute() { + generateAndSetGadgetId(); + } + }); + } + + private void allowModificationOfNewlyCreatedGadget() { + // Missing height attribute indicates freshly added gadget. Assume that the + // document is modified for the purpose of updating attributes. + if (!hasAttribute(LAST_KNOWN_HEIGHT_ATTRIBUTE) && editingIndicator.isEditing()) { + scheduleGadgetIdUpdate(); + onModifyingDocument(); + } + } + + /** + * Creates a widget to render the gadget. + */ + public void createWidget() { + if (isActive()) { + log("Repeated attempt to create gadget widget."); + return; + } + + active = true; + log("Creating Gadget Widget ", getGadgetName()); + + ui.enableMenu(); + allowModificationOfNewlyCreatedGadget(); + setSavedIframeHeight(); + setSavedIframeWidth(); + + source = getAttribute(URL_ATTRIBUTE); + String title = getAttribute(TITLE_ATTRIBUTE); + ui.setTitleLabelText((title == null) ? "" : URL.decodeComponent(title)); + updatePrefsFromAttribute(getAttribute(PREFS_ATTRIBUTE)); + refreshParticipantInformation(); + + // HACK(anorth): This event routing should happen outside the widget. + ObservableConversation conv = (ObservableConversation) blip.getConversation(); + conv.addListener(new WaveletListenerAdapter(blip, this)); + log("Requesting Gadget metadata: ", source); + String cachedIframeUrl = getAttribute(IFRAME_URL_ATTRIBUTE); + if (validIframeUrl(cachedIframeUrl)) { + registerWithController(cleanUrl(cachedIframeUrl), 0, 0); + } + GadgetDataStoreImpl.getInstance().getGadgetData(source, waveletName, getInstanceId(), + new GadgetDataStore.DataCallback() { + @Override + public void onError(String message, Throwable t) { + if ((t != null) && (t.getMessage() != null)) { + message += " " + t.getMessage(); + } + showBrokenGadget(message); + } + + @Override + public void onDataReady(GadgetMetadata metadata, String securityToken) { + if (isActive()) { + ReadableStringSet views = metadata.getViewSet(); + String view = null; + if (views.contains(GADGET_PRIMARY_VIEW)) { + view = GADGET_PRIMARY_VIEW; + } else if (views.contains(GADGET_DEFAULT_VIEW)) { + view = GADGET_DEFAULT_VIEW; + } else if (!views.isEmpty()) { + view = views.someElement(); + } else { + showBrokenGadget("Gadget has no view to render."); + return; + } + String url = metadata.getIframeUrl(view); + if (validIframeUrl(url)) { + constructGadgetFromMetadata(metadata, view, securityToken); + } else { + showBrokenGadget("Invalid IFrame URL " + url); + } + } + } + }); + } + + /** + * Utility function to send setPref RPC to the gadget. + * + * @param target the gadget frame ID. + * @param name name of the preference to set. + * @param value value of the preference. + */ + public native void sendGadgetPrefRpc(String target, String name, String value) /*-{ + try { + $wnd.gadgets.rpc.call(target, 'set_pref', null, 0, name, value); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('set_pref RPC failed'); + } + }-*/; + + /** + * Utility function to send initialization RPC to Podium gadget. + * + * @param target the gadget frame ID. + * @param id Podium ID of this client. + * @param otherId Podium ID of the opponent client. + */ + public native void sendPodiumOnInitializedRpc(String target, String id, String otherId) /*-{ + try { + $wnd.gadgets.rpc.call(target, 'onInitialized', null, id, otherId); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('onInitialized RPC failed'); + } + }-*/; + + /** + * Utility function to send state change RPC to Podium gadget. + * + * @param target the gadget frame ID. + * @param state Podium gadget state. + */ + public native void sendPodiumOnStateChangedRpc(String target, String state) /*-{ + try { + $wnd.gadgets.rpc.call(target, 'onStateChanged', null, state); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('onStateChanged RPC failed'); + } + }-*/; + + /** + * Utility function to send title to the embedding container. + * + * @param title the title value for the container. + */ + public native void sendEmbeddedRpc(String title) /*-{ + try { + $wnd.gadgets.rpc.call(null, 'set_title', null, title); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('set_title RPC failed'); + } + }-*/; + + /** + * Utility function to send participant information to Wave gadget. + * + * @param target the gadget frame ID. + * @param participants JSON string of Wavelet participants. + */ + public native void sendParticipantsRpc(String target, JavaScriptObject participants) /*-{ + try { + $wnd.gadgets.rpc.call(target, 'wave_participants', null, participants); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('wave_participants RPC failed'); + } + }-*/; + + /** + * Utility function to send Gadget state to Wave gadget. + * + * @param target the gadget frame ID. + * @param state JSON string of Gadget state. + */ + public native void sendGadgetStateRpc(String target, JavaScriptObject state) /*-{ + try { + $wnd.gadgets.rpc.call(target, 'wave_gadget_state', null, state); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('wave_gadget_state RPC failed'); + } + }-*/; + + /** + * Utility function to send private Gadget state to Wave gadget. + * + * @param target the gadget frame ID. + * @param state JSON string of Gadget state. + */ + public native void sendPrivateGadgetStateRpc(String target, JavaScriptObject state) /*-{ + try { + $wnd.gadgets.rpc.call(target, 'wave_private_gadget_state', null, state); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('wave_private_gadget_state RPC failed'); + } + }-*/; + + /** + * Utility function to send Gadget mode to Wave gadget. + * + * @param target the gadget frame ID. + * @param mode JSON string of Gadget state. + */ + public native void sendModeRpc(String target, JavaScriptObject mode) /*-{ + try { + $wnd.gadgets.rpc.call(target, 'wave_gadget_mode', null, mode); + } catch (e) { + // HACK(user): Ignoring any failure for now. + @org.waveprotocol.wave.client.gadget.GadgetLog::log(Ljava/lang/String;) + ('wave_gadget_mode RPC failed'); + } + }-*/; + + /** + * Sends the gadget state to the wave gadget. Injects the playback state value + * into the state. + */ + public void sendGadgetState() { + if (waveEnabled) { + log("Sending gadget state: ", state.toJson()); + sendGadgetStateRpc(getGadgetName(), state.asJavaScriptObject()); + } + } + + /** + * Sends the private gadget state to the wave gadget. + */ + public void sendPrivateGadgetState() { + if (waveEnabled) { + String gadgetId = getGadgetId(); + StateMap privateState = StateMap.createFromStringMap(gadgetId != null ? + supplement.getGadgetState(gadgetId) : CollectionUtils.<String> emptyMap()); + log("Sending private gadget state: ", privateState.toJson()); + sendPrivateGadgetStateRpc(getGadgetName(), privateState.asJavaScriptObject()); + } + } + + /** + * Sends the gadget mode to the wave gadget. + */ + public void sendMode() { + if (waveEnabled) { + StateMap mode = StateMap.create(); + mode.put(PLAYBACK_MODE_KEY, "0"); + mode.put(EDIT_MODE_KEY, editingIndicator.isEditing() ? "1" : "0"); + log("Sending gadget mode: ", mode.toJson()); + sendModeRpc(getGadgetName(), mode.asJavaScriptObject()); + } + } + + /** + * Returns the ID of the user who added the gadget as defined in the author + * attribute. If the attribute is not defined returns the blip author instead + * (as the best guess for the author for backward compatibility). + * + * @return author ID of the user who added the gadget to the wave + */ + private String getAuthor() { + String author = element.getAttribute(AUTHOR_ATTRIBUTE); + return (author != null) ? author : blip.getAuthorId().getAddress(); + } + + /** + * Builds a map of participants from two lists of participant ids. + */ + private StringMap<ParticipantId> getParticipantsForIds( + Collection<ParticipantId> list1, Collection<ParticipantId> list2) { + StringMap<ParticipantId> mergedMap = CollectionUtils.createStringMap(); + for (ParticipantId p : list1) { + mergedMap.put(p.getAddress(), p); + } + for (ParticipantId p : list2) { + mergedMap.put(p.getAddress(), p); + } + return mergedMap; + } + + /** + * Refreshes the participant information. + */ + private void refreshParticipantInformation() { + StringMap<ParticipantId> waveletParticipants = getParticipantsForIds( + blip.getConversation().getParticipantIds(), blip.getContributorIds()); + ParticipantId viewerId = new ParticipantId(loginName); + waveletParticipants.put(viewerId.getAddress(), viewerId); + List<ParticipantId> participantList = CollectionUtils.newJavaList(waveletParticipants); + participants = ParticipantInformation.create( + viewerId.getAddress(), getAuthor(), participantList, getUrlPrefix(), profileManager); + final StringBuilder builder = new StringBuilder(); + builder.append("Participants: "); + builder.append("I am " + participants.getMyId()); + for (ParticipantId participant : participantList) { + builder.append("; " + participant); + } + + log(builder.toString()); + } + + /** + * Refreshes and sends participant information to wave-enabled gadget. + */ + private void sendCurrentParticipantInformation() { + if (waveEnabled) { + refreshParticipantInformation(); + sendParticipantsRpc(getGadgetName(), participants); + log("Sent participants: ", participants); + } + } + + /** + * Utility function to perform setPref RPC to the gadget. + * + * @param name name of the preference to set. + * @param value value of the preference. + */ + public void setGadgetPref(final String name, final String value) { + ScheduleCommand.addCommand(new Task() { + @Override + public void execute() { + if (isActive()) { + sendGadgetPrefRpc(getGadgetName(), name, value); + } + } + }); + } + + /** + * Marks the Widget as inactive after the gadget node is removed from the + * parent. + */ + public void setInactive() { + log("Gadget node removed."); + supplement.removeListener(this); + active = false; + } + + @Override + public void setIframeHeight(String height) { + if (!isActive()) { + return; + } + log("Set IFrame height ", height); + try { + int heightValue = parseSizeString(height); + ui.setIframeHeight(heightValue); + scheduleGadgetAttributeUpdate(LAST_KNOWN_HEIGHT_ATTRIBUTE, Long.toString(heightValue)); + } catch (NumberFormatException e) { + log("Invalid height (ignored): ", height); + } + } + + public void setIframeWidth(String width) { + if (!isActive()) { + return; + } + log("Set IFrame width ", width); + try { + int widthValue = parseSizeString(width); + if (widthValue <= 0) { + ui.setIframeWidth("100%"); + } else { + ui.setIframeWidth(widthValue + "px"); + } + ui.makeInline(); + scheduleGadgetAttributeUpdate(LAST_KNOWN_WIDTH_ATTRIBUTE, Long.toString(widthValue)); + } catch (NumberFormatException e) { + log("Invalid width (ignored): ", width); + } + } + + @Override + public void requestNavigateTo(String url) { + log("Requested navigate to: ", url); + // NOTE(user): Currently only allow the gadgets to change the fragment part of the URL. + String newFragment = url.replaceFirst(BEFORE_FRAGMENT_PATTERN, ""); + if (newFragment.matches(FRAGMENT_VALIDATION_PATTERN)) { + Location.replace(Location.getHref().replaceFirst(FRAGMENT_PATTERN, "") + "#" + newFragment); + } else { + log("Navigate request denied."); + } + } + + @Override + public void updatePodiumState(String podiumState) { + if (isActive()) { + modifyState(PODIUM_STATE_NAME, podiumState); + blipSubmitter.submit(); + } + } + + private void setPref(String key, String value) { + if (!canModifyDocument() || (key == null) || (value == null)) { + return; + } + userPrefs.put(key, value); + if (prefElements.containsKey(key)) { + if (!prefElements.get(key).getValue().equals(value)) { + log("Updating preference '", key, "'='", value, "'"); + onModifyingDocument(); + prefElements.get(key).setValue(value); + blipSubmitter.submit(); + } + } else { + log("New preference '", key, "'='", value, "'"); + onModifyingDocument(); + element.getMutableDoc().insertXml( + Point.end((ContentNode)element), GadgetXmlUtil.constructPrefXml(key, value)); + blipSubmitter.submit(); + } + + } + + @Override + public void setPrefs(String ... keyValue) { + // Ignore callbacks from the gadget in playback mode. + if (!canModifyDocument()) { + return; + } + // Ignore the last key if its value is missing. + for (int i = 0; i < keyValue.length - 1; i+=2) { + setPref(keyValue[i], keyValue[i + 1]); + } + } + + /** + * Sets up a polling loop to check the edit mode state and send it to the + * gadget. + * + * TODO(user): Add edit mode change events to the client and find a way to + * relay them to the gadget containers. + */ + private void setupModePolling() { + new ScheduleTimer() { + private boolean wasEditing = editingIndicator.isEditing(); + + @Override + public void run() { + if (!isActive()) { + cancel(); + return; + } else { + boolean newEditing = editingIndicator.isEditing(); + if (wasEditing != newEditing) { + sendMode(); + wasEditing = newEditing; + } + } + } + }.scheduleRepeating(EDITING_POLLING_TIMER_MS); + } + + /** + * HACK: This is a workaround for Firefox bug + * https://bugzilla.mozilla.org/show_bug.cgi?id=498904 Due to this bug the + * gadget RPCs may be sent to a dead iframe. Changing the iframe ID fixes + * container-to-gadget communication. Non-wave gadgets may have other issues + * associated with this bug. But most wave-enabled gadgets should work when + * the iframe ID is updated in the waveEnable call. + */ + private void substituteIframeId() { + clientInstanceId = nextClientInstanceId++; + ui.setIframeId(getGadgetName()); + controllerRegistration(iframeUrl, 0, 0); + } + + @Override + public void waveEnable(String waveApiVersion) { + if (!isActive()) { + return; + } + + // HACK: See substituteIframeId() description. + // TODO(user): Remove when the Firefox bug is fixed. + if (UserAgent.isFirefox()) { + substituteIframeId(); + } + + waveEnabled = true; + this.waveApiVersion = waveApiVersion; + log("Wave-enabled gadget registered with API version ", waveApiVersion); + sendWaveGadgetInitialization(); + setupModePolling(); + } + + @Override + public void waveGadgetStateUpdate(final JavaScriptObject delta) { + // Return if in playback mode. isEditable indicates playback. + if (!canModifyDocument()) { + return; + } + + final StateMap deltaState = StateMap.create(); + deltaState.fromJsonObject(delta); + // Defer state modifications to avoid RPC failure in Safari 3. The + // intermittent failure is caused by RPC called from received RPC + // callback. + // TODO(user): Remove this workaround once this is fixed in GGS. + ScheduleCommand.addCommand(new Task() { + @Override + public void execute() { + deltaState.each(new Each() { + @Override + public void apply(final String key, final String value) { + if (value != null) { + modifyState(key, value); + } else { + deleteState(key); + } + } + }); + log("Applied delta ", delta.toString(), " new state ", state.toJson()); + gadgetStateSubmitter.triggerScheduledSubmit(); + blipSubmitter.submitImmediately(); + } + }); + } + + /** + * Generates a unique gadget ID. + * TODO(user): Replace with proper MD5-based UUID. + * + * @return a unique gadget ID. + */ + private String generateGadgetId() { + String name = ModernIdSerialiser.INSTANCE.serialiseWaveletName(waveletName); + String instanceDescriptor = name + getAuthor() + source; + String prefix = Integer.toHexString(instanceDescriptor.hashCode()); + String time = Integer.toHexString(new Date().hashCode()); + String version = Long.toHexString(blip.getLastModifiedVersion()); + return prefix + time + version; + } + + private String generateAndSetGadgetId() { + if (!canModifyDocument()) { + return null; + } + String id = generateGadgetId(); + element.getMutableDoc().setElementAttribute(element, ID_ATTRIBUTE, id); + return id; + } + + private String getGadgetId() { + return element.getAttribute(ID_ATTRIBUTE); + } + + private String getOrGenerateGadgetId() { + String id = getGadgetId(); + if ((id == null) || id.isEmpty()) { + id = generateAndSetGadgetId(); + } + return id; + } + + @Override + public void wavePrivateGadgetStateUpdate(JavaScriptObject delta) { + // Return if in playback mode. isEditable indicates playback. + if (!canModifyDocument()) { + return; + } + + StateMap deltaState = StateMap.create(); + deltaState.fromJsonObject(delta); + final String gadgetId = getOrGenerateGadgetId(); + if (gadgetId != null) { + deltaState.each(new Each() { + @Override + public void apply(final String key, final String value) { + supplement.setGadgetState(gadgetId, key, value); + } + }); + log("Applied private delta ", deltaState.toJson()); + privateGadgetStateSubmitter.triggerScheduledSubmit(); + } else { + log("Unable to get gadget ID to update private state. Delta ", deltaState.toJson()); + } + } + + private void modifyState(String key, String value) { + if (!canModifyDocument()) { + log("Unable to modify state ", key, " ", value); + } else { + log("Modifying state ", key, " ", value); + if (stateElements.containsKey(key)) { + if (!stateElements.get(key).getValue().equals(value)) { + onModifyingDocument(); + stateElements.get(key).setValue(value); + } + } else { + onModifyingDocument(); + element.getMutableDoc().insertXml( + Point.end((ContentNode)element), GadgetXmlUtil.constructStateXml(key, value)); + } + } + } + + private void deleteState(String key) { + if (!canModifyDocument()) { + log("Unable to remove state ", key); + } else { + log("Removing state ", key); + if (stateElements.containsKey(key)) { + onModifyingDocument(); + element.getMutableDoc().deleteNode(stateElements.get(key).getElement()); + } + } + } + + private void sendWaveGadgetInitialization() { + sendMode(); + sendCurrentParticipantInformation(); + gadgetStateSubmitter.submitImmediately(); + privateGadgetStateSubmitter.submitImmediately(); + // Send participant information one more time as participant pictures may be + // loaded with a delay. There is no callback to get the picture update + // event. + new ScheduleTimer() { + @Override + public void run() { + if (isActive()) { + sendCurrentParticipantInformation(); + } + } + }.schedule(REPEAT_PARTICIPANT_INFORMATION_SEND_DELAY_MS); + } + + private void updateElementMaps( + GadgetElementChild child, StringMap<GadgetElementChild> childMap, StateMap stateMap) { + if (child.getKey() == null) { + log("Missing key attribute: element ignored."); + return; + } + if (childMap.containsKey(child.getKey())) { + logFine("Old value: ", childMap.get(child.getKey())); + } + childMap.put(child.getKey(), child); + stateMap.put(child.getKey(), child.getValue()); + logFine("Updated element ", child.getKey(), " : ", child.getValue()); + } + + private void processTitleChild(GadgetElementChild child) { + titleElement = child; + String newTitleValue = child.getValue(); + if (newTitleValue == null) { + newTitleValue = ""; + } + if (!newTitleValue.equals(ui.getTitleLabelText())) { + ui.setTitleLabelText(newTitleValue); + } + } + + private void removeChildFromMaps( + GadgetElementChild child, StringMap<GadgetElementChild> childMap, StateMap stateMap) { + String key = child.getKey(); + if (childMap.containsKey(key)) { + stateMap.remove(key); + childMap.remove(key); + logFine("Removed element ", key); + } + } + + private void processChild(GadgetElementChild child) { + if (child == null) { + return; + } + logFine("Processing: ", child); + switch (child.getType()) { + case STATE: + updateElementMaps(child, stateElements, state); + break; + case PREF: + updateElementMaps(child, prefElements, userPrefs); + break; + case TITLE: + processTitleChild(child); + break; + case CATEGORIES: + logFine("Categories element ignored."); + break; + default: + // Note(user): editor may add/remove selection and cursor nodes. + logFine("Unexpected gadget node ", child.getTag()); + } + } + + /** + * Finds the first copy of the given child in the sibling sequence starting at + * the given node. + * + * @param child Child to find next copy of. + * @param node Node to scan from. + * @return Next copy of the child or null if not found. + */ + private static GadgetElementChild findNextChildCopy(GadgetElementChild child, ContentNode node) { + if (child == null) { + return null; + } + while (node != null) { + GadgetElementChild gadgetChild = GadgetElementChild.create(node); + if (child.isDuplicate(gadgetChild)) { + return gadgetChild; + } + node = node.getNextSibling(); + } + return null; + } + + /** + * Task removes redundant nodes that match redundantNodeCheckChild. + */ + private final Scheduler.Task removeRedundantNodesTask = new Scheduler.Task() { + @Override + public void execute() { + if (!canModifyDocument()) { + return; + } + if (redundantNodeCheckChild != null) { + GadgetElementChild firstMatchingNode = findNextChildCopy( + redundantNodeCheckChild, element.getFirstChild()); + GadgetElementChild lastSeenNode = firstMatchingNode; + while (lastSeenNode != null) { + lastSeenNode = findNextChildCopy( + redundantNodeCheckChild, firstMatchingNode.getElement().getNextSibling()); + if (lastSeenNode != null) { + log("Removing: ", lastSeenNode); + element.getMutableDoc().deleteNode(lastSeenNode.getElement()); + } + } + } else { + log("Undefined redundant node check child."); + } + redundantNodeCheckChild = null; + } + }; + + /** + * Scans nodes and removes duplicate copies of the given child leaving only + * the first copy. + * TODO(user): Unit test for node manipulations. + * + * @param child Child to delete the duplicates of. + */ + private void removeRedundantNodes(final GadgetElementChild child) { + if (!documentModified || (child == null)) { + return; + } + if (redundantNodeCheckChild == null) { + redundantNodeCheckChild = child; + ScheduleCommand.addCommand(removeRedundantNodesTask); + } else { + log("Overlapping redundant node check requests."); + } + } + + private final ElementChangeTask childAddedTask = new ElementChangeTask() { + @Override + void processChange(ContentNode node) { + GadgetElementChild child = GadgetElementChild.create(node); + log("Added: ", child); + if (child != null) { + removeRedundantNodes(child); + processChild(child); + } + } + }; + + /** + * Processes an add child event. + * + * @param node the child added to the gadget node. + */ + public void onChildAdded(ContentNode node) { + childAddedTask.run(node); + } + + private final ElementChangeTask childRemovedTask = new ElementChangeTask() { + @Override + void processChange(ContentNode node) { + GadgetElementChild child = GadgetElementChild.create(node); + log("Removed: ", child); + switch (child.getType()) { + case STATE: + removeChildFromMaps(child, stateElements, state); + break; + case PREF: + removeChildFromMaps(child, prefElements, userPrefs); + break; + case TITLE: + log("Removing title is not supported"); + break; + case CATEGORIES: + log("Removing categories is not supported"); + break; + default: + // Note(user): editor may add/remove selection and cursor nodes. + log("Unexpected gadget node removed ", child.getTag()); + } + } + }; + + /** + * Processes a remove child event. + * + * @param node + */ + public void onRemovingChild(ContentNode node) { + childRemovedTask.run(node); + } + + /** + * Rescans all gadget children to update the values stored in the gadget + * object. + */ + private void rescanGadgetXmlElements() { + log("Rescanning elements"); + ContentNode childNode = element.getFirstChild(); + while (childNode != null) { + processChild(GadgetElementChild.create(childNode)); + childNode = childNode.getNextSibling(); + } + } + + private final ElementChangeTask descendantsMutatedTask = new ElementChangeTask() { + @Override + void processChange(ContentNode node) { + rescanGadgetXmlElements(); + } + }; + + private final Scheduler.Task schedulableMutationTask = new Scheduler.Task() { + @Override + public void execute() { + descendantsMutatedTask.run(null); + } + }; + + /** + * Processes a mutation event. + */ + public void onDescendantsMutated() { + log("Descendants mutated."); + ScheduleCommand.addCommand(schedulableMutationTask); + } + + @Override + public void onBlipContributorAdded(ParticipantId contributor) { + if (isActive()) { + log("Contributor added ", contributor); + sendCurrentParticipantInformation(); + } else { + log("Contributor added event in deleted node."); + } + } + + @Override + public void onBlipContributorRemoved(ParticipantId contributor) { + if (isActive()) { + log("Contributor removed ", contributor); + sendCurrentParticipantInformation(); + } else { + log("Contributor removed event in deleted node."); + } + } + + @Override + public void onParticipantAdded(ParticipantId participant) { + if (isActive()) { + log("Participant added ", participant); + sendCurrentParticipantInformation(); + } else { + log("Participant added event in deleted node."); + } + } + + @Override + public void onParticipantRemoved(ParticipantId participant) { + if (isActive()) { + log("Participant removed ", participant); + sendCurrentParticipantInformation(); + } else { + log("Participant removed event in deleted node."); + } + } + + private Object[] expandArgs(Object object, Object ... objects) { + Object[] args = new Object[objects.length + 1]; + args[0] = object; + System.arraycopy(objects, 0, args, 1, objects.length); + return args; + } + + private void log(Object ... objects) { + if (GadgetLog.shouldLog()) { + GadgetLog.logLazy(expandArgs(clientInstanceLogLabel, objects)); + } + } + + private void logFine(Object ... objects) { + if (GadgetLog.shouldLogFine()) { + GadgetLog.logFineLazy(expandArgs(clientInstanceLogLabel, objects)); + } + } + + /** + * Returns the URL of the client including protocol and host. + * + * @return URL of the client. + */ + private String getUrlPrefix() { + return Location.getProtocol() + "//" + Location.getHost(); + } + + /** + * Returns the UI element. + * + * @return UI element. + */ + Element getElement() { + return ui.getElement(); + } + + private boolean isActive() { + return active; + } + + private boolean canModifyDocument() { + return isActive(); + } + + @Override + public void deleteGadget() { + if (canModifyDocument()) { + element.getMutableDoc().deleteNode(element); + } + } + + @Override + public void selectGadget() { + if (isActive()) { + CMutableDocument doc = element.getMutableDoc(); + element.getSelectionHelper().setSelectionPoints( + Point.before(doc, element), Point.after(doc, element)); + } + } + + @Override + public void resetGadget() { + if (canModifyDocument()) { + state.each(new Each() { + @Override + public void apply(String key, String value) { + deleteState(key); + } + }); + gadgetStateSubmitter.submit(); + final String gadgetId = getGadgetId(); + if (gadgetId != null) { + supplement.getGadgetState(gadgetId).each(new ProcV<String>() { + @Override + public void apply(String key, String value) { + supplement.setGadgetState(gadgetId, key, null); + } + }); + privateGadgetStateSubmitter.submit(); + } + } + } + + private static native void excludeCssName() /*-{ + css(); + }-*/; + + private static class BlipEditingIndicator implements EditingIndicator { + private final ContentElement element; + + /** + * Constructs editing indicator for the gadget's blip. + */ + BlipEditingIndicator(ContentElement element) { + this.element = element; + } + + /** + * Returns the current edit state of the blip. + * TODO(user): add event-driven update of the edit state. + * + * @return whether the blip is in edit state. + */ + @Override + public boolean isEditing() { + return (element != null) + ? AnnotationPainter.isInEditingDocument(ContentElement.ELEMENT_MANAGER, element) : false; + } + } + + @Override + public void onMaybeGadgetStateChanged(String gadgetId) { + if (gadgetId != null) { + String myId = getGadgetId(); + if (gadgetId.equals(myId)) { + privateGadgetStateSubmitter.submitImmediately(); + } + } + } + + /** + * Executes when the document is being modified in response to a user action. + */ + private void onModifyingDocument() { + documentModified = true; + if (toUpdateIframeUrl) { + scheduleGadgetAttributeUpdate(IFRAME_URL_ATTRIBUTE, iframeUrl); + toUpdateIframeUrl = false; + } + } + + /** + * Creates GadgetWidget instance with preset fields for testing. + * + * TODO(user): Refactor to remove test code. + * + * @param id client instance ID + * @param userPrefs user prederences + * @param waveletName wavelet name + * @param securityToken security token + * @param locale locale + * @return test instance of the widget + */ + @VisibleForTesting + static GadgetWidget createForTesting(int id, GadgetUserPrefs userPrefs, WaveletName waveletName, + String securityToken, Locale locale) { + GadgetWidget widget = new GadgetWidget(); + widget.clientInstanceId = id; + widget.userPrefs = userPrefs; + widget.waveletName = waveletName; + widget.securityToken = securityToken; + widget.locale = locale; + return widget; + } + + /** + * @return RPC token for testing + */ + @VisibleForTesting + String getRpcToken() { + return rpcToken; + } +} Added: trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.java =================================================================== --- trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.java (rev 0) +++ trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.java 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,499 @@ +// @formatter:off +/** + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.waveprotocol.wave.client.gadget.renderer; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.IFrameElement; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.StyleInjector; +import com.google.gwt.event.dom.client.DomEvent; +import com.google.gwt.event.dom.client.MouseOutEvent; +import com.google.gwt.event.dom.client.MouseOutHandler; +import com.google.gwt.event.dom.client.MouseOverEvent; +import com.google.gwt.event.dom.client.MouseOverHandler; +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.resources.client.DataResource; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.resources.client.ImageResource.ImageOptions; +import com.google.gwt.resources.client.ImageResource.RepeatStyle; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.NamedFrame; +import com.google.gwt.user.client.ui.Widget; + +import org.waveprotocol.wave.client.common.util.DomHelper; +import org.waveprotocol.wave.client.editor.EditorStaticDeps; +import org.waveprotocol.wave.client.scheduler.ScheduleTimer; +import org.waveprotocol.wave.client.widget.common.ClickableDivPanel; +import org.waveprotocol.wave.client.widget.common.HoverHelper; +import org.waveprotocol.wave.client.widget.common.HoverHelper.Hoverable; +import org.waveprotocol.wave.client.widget.menu.MenuButton; + +/** + * GadgetWidget UI implementation. + * + */ +public class GadgetWidgetUi extends Composite implements Hoverable { + /** + * Class that exposes protected addDomHandler method from NamedFrame as + * addHandler. Serves as the Gadget IFrame widget class. + */ + private static class GadgetIFrame extends NamedFrame { + GadgetIFrame(String name) { + super(name); + } + + <H extends EventHandler> HandlerRegistration addHandler( + final H handler, DomEvent.Type<H> type) { + return addDomHandler(handler, type); + } + } + + /** + * CSS resource for the style injector. + */ + public interface Resources extends ClientBundle { + /** The singleton instance of our CSS resources. */ + static final Resources RESOURCES = GWT.<Resources> create(Resources.class); + + @Source("Gadget.css") + public Css css(); + + @Source("broken_gadget.png") + DataResource brokenGadget(); + + @Source("loading_gadget.gif") + ImageResource loadingGadgetLarge(); + + @Source("loading_gadget_small.gif") + ImageResource loadingGadgetSmall(); + + @Source("meta_more.png") + ImageResource moreImage(); + + // Normal + @Source("meta_left.png") + ImageResource metaLeftImage(); + + @Source("meta_right.png") + ImageResource metaRightImage(); + + static final String META_LEFT_WIDTH = "-" + RESOURCES.metaLeftImage().getWidth() + "px"; + static final String META_RIGHT_WIDTH = "-" + RESOURCES.metaRightImage().getWidth() + "px"; + static final String FADE_DELAY_STRING = FADE_DELAY_MS + "ms"; + + @Source("meta_mid.png") + @ImageOptions(repeatStyle = RepeatStyle.Horizontal) + ImageResource metaMid(); + + // Down + @Source("meta_left_down.png") + ImageResource metaLeftDown(); + + @Source("meta_right_down.png") + ImageResource metaRightDown(); + + @Source("meta_mid_down.png") + ImageResource metaMidDown(); + + // Hover + @Source("meta_left_hover.png") + ImageResource metaLeftHover(); + + @Source("meta_right_hover.png") + ImageResource metaRightHover(); + + @Source("meta_mid_hover.png") + ImageResource metaMidHover(); + } + + /** CSS interface with style string methods. */ + interface Css extends CssResource { + String panel(); + String inline(); + String title(); + String gadgetFrame(); + String iframeDiv(); + String loadedGadgetFrame(); + String loadingGadgetFrame(); + String loadingGadgetSmallThrobber(); + String loadingGadgetLargeThrobber(); + String loadedGadget(); + String brokenGadgetIcon(); + String more(); + String metaButton(); + String metaRight(); + String metaButtonsPanel(); + String metaLeft(); + String metaButtons(); + String disabled(); + } + + /** The singleton instance of our CSS resources. */ + static final Css CSS = Resources.RESOURCES.css(); + + /** + * Static initializer: injects stylesheet. + */ + static { + StyleInjector.inject(CSS.getText()); + } + + @UiField ClickableDivPanel enclosingBox; + + @UiField ClickableDivPanel metaButtons; + + @UiField ClickableDivPanel metaLeft; + + @UiField ClickableDivPanel metaButtonsPanel; + + @UiField ClickableDivPanel metaRight; + + @UiField Label titleLabel; + + @UiField ClickableDivPanel gadgetFrame; + + @UiField ClickableDivPanel iframeDiv; + + interface Binder extends UiBinder<ClickableDivPanel, GadgetWidgetUi> {} + + private static final Binder BINDER = GWT.create(Binder.class); + + /** + * The maximum height of the gadget frame that should use the small throbber + * icon. + */ + private static final int MAX_SMALL_GADGET_HEIGHT = 63; + + private static final int FADE_DELAY_MS = 300; + + /** Gadget IFrame. */ + private final GadgetIFrame gadgetIframe; + + private GadgetUiListener listener = null; + + private MenuButton menu; + + private boolean menuEnabled; + + private final EditingIndicator editingIndicator; + + private static enum ThrobberState { + SMALL, + LARGE, + BROKEN, + DISABLED + } + + private ThrobberState throbberState = ThrobberState.SMALL; + + /** + * Constructs gadget widget UI. + */ + public GadgetWidgetUi(String gadgetName, EditingIndicator editingIndicator) { + this.editingIndicator = editingIndicator; + menuEnabled = false; + initWidget(BINDER.createAndBindUi(this)); + gadgetIframe = new GadgetIFrame(gadgetName); + buildIFrame(gadgetName); + buildMenu(); + hideMetaButtons(); + gadgetFrame.addStyleName(CSS.loadingGadgetFrame()); + makeWidgetsUnselectable(); + HoverHelper.getInstance().setup(this); + } + + private void makeWidgetsUnselectable() { + makeUnselectable(enclosingBox); + makeUnselectable(titleLabel); + makeUnselectable(gadgetFrame); + makeUnselectable(gadgetIframe); + makeUnselectable(metaButtons); + makeUnselectable(metaLeft); + makeUnselectable(metaButtonsPanel); + makeUnselectable(metaRight); + makeUnselectable(menu.getButton()); + } + + private void makeUnselectable(Widget widget) { + DomHelper.setContentEditable(widget.getElement(), false, false); + DomHelper.makeUnselectable(widget.getElement()); + } + + private void buildIFrame(String gadgetName) { + gadgetIframe.getElement().setId(gadgetName); + + int height = 0; + switch (throbberState) { + case SMALL: + gadgetIframe.addStyleName(CSS.loadingGadgetSmallThrobber()); + height = Resources.RESOURCES.loadingGadgetSmall().getHeight(); + break; + case LARGE: + gadgetIframe.addStyleName(CSS.loadingGadgetLargeThrobber()); + height = Resources.RESOURCES.loadingGadgetLarge().getHeight(); + break; + } + + IFrameElement iframe = getIframeElement(); + iframe.setAttribute("vspace", "0"); + iframe.setAttribute("hspace", "0"); + iframe.setAttribute("frameBorder", "no"); + iframe.setAttribute("moduleId", gadgetName); + iframe.setAttribute("display", "block"); + iframe.setAttribute("height", height + "px"); + // TODO(user): scrolling policy/settings for the wave gadgets. + iframe.setScrolling("no"); + iframeDiv.add(gadgetIframe); + } + + private void buildMenu() { + // TODO(user): Use menu builder. + menu = new MenuButton(CSS.metaButton() + " " + CSS.more(), "Gadget menu"); + menu.addItem("Delete gadget", new Command() { + @Override + public void execute() { + if (listener != null) { + listener.deleteGadget(); + } + } + }, true); + menu.addItem("Select", new Command() { + @Override + public void execute() { + if (listener != null) { + listener.selectGadget(); + } + } + }, true); + menu.addItem("Reset", new Command() { + @Override + public void execute() { + if (listener != null) { + listener.resetGadget(); + } + } + }, true); + metaButtonsPanel.add(menu.getButton()); + } + + /** + * Task to set display:none to deactivate the menu. Note that opacity 0 leaves + * invisible, but active element. + */ + private final ScheduleTimer disableMenuTask = new ScheduleTimer() { + @Override + public void run() { + metaButtons.getElement().getStyle().setDisplay(Display.NONE); + } + }; + + private void hideMetaButtons() { + metaButtons.getElement().getStyle().setOpacity(0); + disableMenuTask.schedule(FADE_DELAY_MS); + } + + private void showMetaButtons() { + disableMenuTask.cancel(); + metaButtons.getElement().getStyle().clearDisplay(); + metaButtons.getOffsetWidth(); // Force the browser to render the first frame. + metaButtons.getElement().getStyle().clearOpacity(); + } + + /** + * Gets the gadget iframe element. + * + * @return Gadget iframe element. + */ + public IFrameElement getIframeElement() { + return IFrameElement.as(gadgetIframe.getElement()); + } + + /** + * Sets the iframe height attribute. + * + * @param height New iframe height. + */ + public void setIframeHeight(long height) { + getIframeElement().setAttribute("height", height + "px"); + if (height > MAX_SMALL_GADGET_HEIGHT) { + if (throbberState == ThrobberState.SMALL) { + gadgetIframe.removeStyleName(CSS.loadingGadgetSmallThrobber()); + gadgetIframe.addStyleName(CSS.loadingGadgetLargeThrobber()); + throbberState = ThrobberState.LARGE; + } + } else { + if (throbberState == ThrobberState.LARGE) { + gadgetIframe.removeStyleName(CSS.loadingGadgetLargeThrobber()); + gadgetIframe.addStyleName(CSS.loadingGadgetSmallThrobber()); + throbberState = ThrobberState.SMALL; + } + } + } + + /** + * Sets the iframe width attribute. + * + * @param width New iframe width. + */ + public void setIframeWidth(String width) { + getIframeElement().setAttribute("width", width); + iframeDiv.setWidth(width); + gadgetFrame.setWidth(width); + } + + /** + * Sets gadget iframe ID. + * + * @param newId new gadget iframe id. + */ + public void setIframeId(String newId) { + gadgetIframe.getElement().setId(newId); + } + + /** + * Sets the iframe source attribute. + * + * @param source New iframe source. + */ + public void setIframeSource(String source) { + if ((source != null) && !source.equals(getIframeElement().getSrc())) { + getIframeElement().setSrc(source); + } + } + + /** + * Removes the gadget loading throbber by replacing the iframe CSS. + */ + public void removeThrobber() { + switch (throbberState) { + case SMALL: + gadgetIframe.removeStyleName(CSS.loadingGadgetSmallThrobber()); + break; + case LARGE: + gadgetIframe.removeStyleName(CSS.loadingGadgetLargeThrobber()); + break; + } + throbberState = ThrobberState.DISABLED; + gadgetIframe.addStyleName(CSS.loadedGadget()); + gadgetFrame.removeStyleName(CSS.loadingGadgetFrame()); + gadgetFrame.addStyleName(CSS.loadedGadgetFrame()); + } + + /** + * Shows broken gadget icon and error message. + */ + public void showBrokenGadget(String error) { + switch (throbberState) { + case SMALL: + gadgetIframe.removeStyleName(CSS.loadingGadgetSmallThrobber()); + getIframeElement().setAttribute("height", (MAX_SMALL_GADGET_HEIGHT + 1) + "px"); + break; + case LARGE: + gadgetIframe.removeStyleName(CSS.loadingGadgetLargeThrobber()); + break; + } + throbberState = ThrobberState.BROKEN; + gadgetIframe.addStyleName(CSS.brokenGadgetIcon()); + } + + public String getTitleLabelText() { + return titleLabel.getText(); + } + + /** + * Sets the text of the title label. + * + * @param text New title label text. + */ + public void setTitleLabelText(String text) { + // TODO(user): Remove when the editor ignores mutations from doodads. + EditorStaticDeps.startIgnoreMutations(); + try { + titleLabel.setText(text); + } finally { + EditorStaticDeps.endIgnoreMutations(); + } + } + + public void setGadgetUiListener(GadgetUiListener listener) { + this.listener = listener; + } + + /** + * Enables the overlay Gadget menu. + */ + public void enableMenu() { + menuEnabled = true; + } + + /** + * Disables the overlay Gadget menu. + */ + public void disableMenu() { + hideMetaButtons(); + menu.onOff(menu.getButton()); + menuEnabled = false; + } + + public void makeInline() { + enclosingBox.addStyleName(CSS.inline()); + } + + @Override + public void onMouseEnter() { + if (!menuEnabled || !editingIndicator.isEditing()) { + return; + } + showMetaButtons(); + } + + @Override + public void onMouseLeave() { + hideMetaButtons(); + } + + @Override + public void addHandlers(MouseOverHandler mouseOverHandler, MouseOutHandler mouseOutHandler) { + // TODO(user): Investigate why the event propagation does not work and + // remove unnecessary handler setup. + addDomHandler(mouseOverHandler, MouseOverEvent.getType()); + addDomHandler(mouseOutHandler, MouseOutEvent.getType()); + enclosingBox.addMouseOverHandler(mouseOverHandler); + gadgetFrame.addMouseOverHandler(mouseOverHandler); + iframeDiv.addMouseOverHandler(mouseOverHandler); + gadgetIframe.addHandler(mouseOverHandler, MouseOverEvent.getType()); + metaButtons.addMouseOverHandler(mouseOverHandler); + metaLeft.addMouseOverHandler(mouseOverHandler); + metaButtonsPanel.addMouseOverHandler(mouseOverHandler); + metaRight.addMouseOverHandler(mouseOverHandler); + menu.getButton().addMouseOverHandler(mouseOverHandler); + } + + @Override + public boolean isOrHasChild(Element e) { + return enclosingBox.getElement().isOrHasChild(e); + } +} Added: trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.ui.xml =================================================================== --- trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.ui.xml (rev 0) +++ trunk/src/main/java/org/waveprotocol/wave/client/gadget/renderer/GadgetWidgetUi.ui.xml 2012-03-08 15:05:45 UTC (rev 1748) @@ -0,0 +1,49 @@ +<!-- + UI binder for Gadget Container. + Author: [email protected] (Vadim Gerasimov) +--> +<ui:UiBinder + xmlns:ui='urn:ui:com.google.gwt.uibinder' + xmlns:gwt='urn:import:com.google.gwt.user.client.ui' + xmlns:wb='urn:import:org.waveprotocol.wave.client.widget.common' +> + <ui:with + field='res' + type='org.waveprotocol.wave.client.gadget.renderer.GadgetWidgetUi.Resources' + /> + <wb:ClickableDivPanel + ui:field='enclosingBox' + addStyleNames='{res.css.panel}' + > + <!-- Gadget title label. --> + <gwt:Label ui:field='titleLabel' addStyleNames='{res.css.title}'/> + <wb:ClickableDivPanel + ui:field='gadgetFrame' + addStyleNames='{res.css.gadgetFrame}' + > + <!-- Main gadget iframe. --> + <wb:ClickableDivPanel + ui:field='iframeDiv' + addStyleNames='{res.css.iframeDiv}' + /> + <!-- Mouse-over overlay menu. --> + <wb:ClickableDivPanel + ui:field='metaButtons' + addStyleNames='{res.css.metaButtons}' + > + <wb:ClickableDivPanel + ui:field='metaLeft' + addStyleNames='{res.css.metaLeft}' + /> + <wb:ClickableDivPanel + ui:field='metaButtonsPanel' + addStyleNames='{res.css.metaButtonsPanel}' + /> + <wb:ClickableDivPanel + ui:field='metaRight' + addStyleNames='{res.css.metaRight}' + /> + </wb:ClickableDivPanel> + </wb:ClickableDivPanel> + </wb:ClickableDivPanel> +</ui:UiBinder> Added: trunk/src/main/resources/icons/chain-close-black.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/chain-close-black.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/chain-close-white.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/chain-close-white.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/chain-closed-grey.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/chain-closed-grey.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/chain-open-black.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/chain-open-black.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/chain-open-grey.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/chain-open-grey.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/chain-open-white.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/chain-open-white.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/picture-black.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/picture-black.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/picture-grey.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/picture-grey.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Added: trunk/src/main/resources/icons/picture-white.png =================================================================== (Binary files differ) Property changes on: trunk/src/main/resources/icons/picture-white.png ___________________________________________________________________ Name: svn:mime-type + application/octet-stream Copied: trunk/src/main/resources/icons/pref-black.png (from rev 1747, trunk/src/main/java/cc/kune/core/client/resources/pref-black.png) =================================================================== (Binary files differ) Copied: trunk/src/main/resources/icons/pref-grey.png (from rev 1747, trunk/src/main/java/cc/kune/core/client/resources/pref-grey.png) =================================================================== (Binary files differ) Copied: trunk/src/main/resources/icons/pref-white.png (from rev 1747, trunk/src/main/java/cc/kune/core/client/resources/pref-white.png) =================================================================== (Binary files differ) Modified: trunk/src/main/webapp/others/kune-client-actions.xml =================================================================== --- trunk/src/main/webapp/others/kune-client-actions.xml 2012-03-07 13:08:17 UTC (rev 1747) +++ trunk/src/main/webapp/others/kune-client-actions.xml 2012-03-08 15:05:45 UTC (rev 1748) @@ -424,39 +424,6 @@ </guiActionDescriptor> - <guiActionDescriptor> - <type>wave-gadget</type> - <extensionName>WavePlan</extensionName> - <name>New meet planer</name> - <path>New Gadget»Calendar</path> - <description>This gadget helps you to coordinate multi-participant schedules and make decisions in an intuitive manner. - </description> - <new-content-title>New meet planner</new-content-title> - <new-content-textintro></new-content-textintro> - <enabled>true</enabled> - <typeIds> - <typeId origTypeId="docs.folder" destTypeId="docs.doc"/> - <typeId origTypeId="docs.root" destTypeId="docs.doc"/> - <typeId origTypeId="blogs.blog" destTypeId="blogs.post"/> - <typeId origTypeId="lists.list" destTypeId="lists.post"/> - <!-- Add gadget in docs directly: --> - <typeId origTypeId="docs.doc" destTypeId="docs.doc"/> - <typeId origTypeId="blogs.post" destTypeId="blogs.post"/> - <typeId origTypeId="lists.post" destTypeId="lists.post"/> - <typeId origTypeId="wiki.wikipage" destTypeId="wiki.wikipage"/> - <typeId origTypeId="tasks.task" destTypeId="tasks.task"/> - </typeIds> - <rol> - <!-- Administrator, Editor, Viewer --> - <rolRequired>Editor</rolRequired> - <authNeed>true</authNeed> - </rol> - <!-- only for new waves: anyone, onlymembers, onlyadmins, ask (not - used yet) --> - <participants>ask</participants> - </guiActionDescriptor> - - <!-- Media ================================================== --> <guiActionDescriptor>
participants (1)
-
Vicente J. Ruiz Jurado