Bug 661388: Support selecting text in web content [r=mbrubeck]
☠☠ backed out by a87ee7550f6a ☠ ☠
authorMark Finkle <mfinkle@mozilla.com>
Thu, 23 Jun 2011 17:12:38 -0400
changeset 71883 dbd02a315176a1b25bbb7b705fef21a376097c8f
parent 71882 ad3a1bf1bcc300de7db0bff6bbf5391e7e12a793
child 71884 c8f01a0d54822877edac39b04392c64d2e2c977d
push idunknown
push userunknown
push dateunknown
reviewersmbrubeck
bugs661388
milestone7.0a1
Bug 661388: Support selecting text in web content [r=mbrubeck]
mobile/chrome/content/bindings/browser.xml
mobile/chrome/content/browser-scripts.js
mobile/chrome/content/browser.js
mobile/chrome/content/browser.xul
mobile/chrome/content/common-ui.js
mobile/chrome/content/content.js
mobile/locales/en-US/chrome/browser.properties
mobile/themes/core/browser.css
mobile/themes/core/gingerbread/browser.css
mobile/themes/core/gingerbread/images/handle-end.png
mobile/themes/core/gingerbread/images/handle-start.png
mobile/themes/core/images/handle-end.png
mobile/themes/core/images/handle-start.png
mobile/themes/core/jar.mn
--- a/mobile/chrome/content/bindings/browser.xml
+++ b/mobile/chrome/content/bindings/browser.xml
@@ -525,24 +525,36 @@
 
       <!-- Change client coordinates in device pixels to page-relative ones in CSS px. -->
       <method name="transformClientToBrowser">
         <parameter name="clientX"/>
         <parameter name="clientY"/>
         <body>
           <![CDATA[
             let bcr = this.getBoundingClientRect();
-            let view = this.getRootView();
             let scroll = this.getRootView().getPosition();
             return { x: (clientX + scroll.x - bcr.left) / this.scale,
                      y: (clientY + scroll.y - bcr.top) / this.scale };
           ]]>
         </body>
       </method>
 
+      <method name="transformBrowserToClient">
+        <parameter name="browserX"/>
+        <parameter name="browserY"/>
+        <body>
+          <![CDATA[
+            let bcr = this.getBoundingClientRect();
+            let scroll = this.getRootView().getPosition();
+            return { x: (browserX * this.scale - scroll.x + bcr.left) ,
+                     y: (browserY * this.scale - scroll.y + bcr.top)};
+          ]]>
+        </body>
+      </method>
+
       <constructor>
         <![CDATA[
           this._frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
           this._contentViewManager = this._frameLoader.QueryInterface(Components.interfaces.nsIContentViewManager);
 
           let prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                               .getService(Components.interfaces.nsIPrefService)
                                               .QueryInterface(Components.interfaces.nsIPrefBranch2);
--- a/mobile/chrome/content/browser-scripts.js
+++ b/mobile/chrome/content/browser-scripts.js
@@ -66,16 +66,17 @@ XPCOMUtils.defineLazyGetter(this, "Commo
   Services.scriptloader.loadSubScript("chrome://browser/content/common-ui.js", CommonUI);
   return CommonUI;
 });
 
 [
   ["FullScreenVideo"],
   ["BadgeHandlers"],
   ["ContextHelper"],
+  ["SelectionHelper"],
   ["FormHelperUI"],
   ["FindHelperUI"],
   ["NewTabPopup"],
   ["PageActions"],
   ["BrowserSearch"],
   ["CharsetMenu"]
 ].forEach(function (aObject) {
   XPCOMUtils.defineLazyGetter(window, aObject, function() {
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -1749,21 +1749,23 @@ const ContentTouchHandler = {
     let json = aMessage.json;
     if (json.messageId != this._messageId)
       return;
 
     switch (aMessage.name) {
       case "Browser:ContextMenu":
         // Long tap
         let contextMenu = { name: aMessage.name, json: json, target: aMessage.target };
-        if (ContextHelper.showPopup(contextMenu)) {
-          // Stop all input sequences
-          let event = document.createEvent("Events");
-          event.initEvent("CancelTouchSequence", true, false);
-          document.dispatchEvent(event);
+        if (!SelectionHelper.showPopup(contextMenu)) {
+          if (ContextHelper.showPopup(contextMenu)) {
+            // Stop all input sequences
+            let event = document.createEvent("Events");
+            event.initEvent("CancelTouchSequence", true, false);
+            document.dispatchEvent(event);
+          }
         }
         break;
       case "Browser:CaptureEvents": {
         let tab = Browser.getTabForBrowser(aMessage.target);
         tab.contentMightCaptureMouse = json.contentMightCaptureMouse;
         if (this.touchTimeout) {
           clearTimeout(this.touchTimeout);
           this.touchTimeout = null;
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -658,16 +658,19 @@
           <richlistitem class="context-command" id="context-select-input" type="input-text" onclick="ContextCommands.selectInput();">
             <label value="&inputMethod.label;"/>
           </richlistitem>
 #endif
         </richlistbox>
       </vbox>
     </hbox>
 
+    <toolbarbutton id="selectionhandle-start" label="^" left="0" top="0" hidden="true"/>
+    <toolbarbutton id="selectionhandle-end" label="^" left="0" top="0" hidden="true"/>
+
     <hbox id="menulist-container" class="window-width window-height context-block" top="0" left="0" hidden="true" flex="1">
       <vbox id="menulist-popup" class="dialog-dark">
         <label id="menulist-title" class="options-title" crop="center" flex="1"/>
         <richlistbox id="menulist-commands" class="action-buttons" onclick="if (event.target != this) MenuListHelperUI.selectByIndex(this.selectedIndex);" flex="1"/>
       </vbox>
     </hbox>
 
     <!-- alerts for content -->
--- a/mobile/chrome/content/common-ui.js
+++ b/mobile/chrome/content/common-ui.js
@@ -1230,16 +1230,176 @@ var ContextHelper = {
         aEvent.preventDefault();
         if (aEvent.keyCode != aEvent.DOM_VK_ESCAPE)
           this.hide();
         break;
     }
   }
 };
 
+var SelectionHelper = {
+  popupState: null,
+  target: null,
+  deltaX: -1,
+  deltaY: -1,
+
+  get _start() {
+    delete this._start;
+    return this._start = document.getElementById("selectionhandle-start");
+  },
+
+  get _end() {
+    delete this._end;
+    return this._end = document.getElementById("selectionhandle-end");
+  },
+
+  showPopup: function ch_showPopup(aMessage) {
+    if (aMessage.json.types.indexOf("content-text") == -1)
+      return false;
+
+    this.popupState = aMessage.json;
+    this.popupState.target = aMessage.target;
+
+    this._start.customDragger = {
+      isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
+      dragStart: function dragStart(cx, cy, target, scroller) {},
+      dragStop: function dragStop(dx, dy, scroller) { return false; },
+      dragMove: function dragMove(dx, dy, scroller) { return false; }
+    };
+
+    this._end.customDragger = {
+      isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
+      dragStart: function dragStart(cx, cy, target, scroller) {},
+      dragStop: function dragStop(dx, dy, scroller) { return false; },
+      dragMove: function dragMove(dx, dy, scroller) { return false; }
+    };
+
+    this._start.addEventListener("TapDown", this, true);
+    this._start.addEventListener("TapUp", this, true);
+
+    this._end.addEventListener("TapDown", this, true);
+    this._end.addEventListener("TapUp", this, true);
+
+    messageManager.addMessageListener("Browser:SelectionRange", this);
+    messageManager.addMessageListener("Browser:SelectionCopied", this);
+
+    Services.prefs.setBoolPref("accessibility.browsewithcaret", true);
+    this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionStart", { x: this.popupState.x, y: this.popupState.y });
+
+    BrowserUI.pushPopup(this, [this._start, this._end]);
+
+    // Hide the selection handles
+    window.addEventListener("resize", this, true);
+    window.addEventListener("keypress", this, true);
+    Elements.browsers.addEventListener("URLChanged", this, true);
+    Elements.browsers.addEventListener("SizeChanged", this, true);
+    Elements.browsers.addEventListener("ZoomChanged", this, true);
+
+    let event = document.createEvent("Events");
+    event.initEvent("CancelTouchSequence", true, false);
+    this.popupState.target.dispatchEvent(event);
+
+    return true;
+  },
+
+  hide: function ch_hide() {
+    if (this._start.hidden)
+      return;
+
+    this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionEnd", {});
+    this.popupState = null;
+    Services.prefs.setBoolPref("accessibility.browsewithcaret", false);
+
+    this._start.hidden = true;
+    this._end.hidden = true;
+
+    this._start.removeEventListener("TapDown", this, true);
+    this._start.removeEventListener("TapUp", this, true);
+
+    this._end.removeEventListener("TapDown", this, true);
+    this._end.removeEventListener("TapUp", this, true);
+
+    messageManager.removeMessageListener("Browser:SelectionRange", this);
+
+    window.removeEventListener("resize", this, true);
+    window.removeEventListener("keypress", this, true);
+    Elements.browsers.removeEventListener("URLChanged", this, true);
+    Elements.browsers.removeEventListener("SizeChanged", this, true);
+    Elements.browsers.removeEventListener("ZoomChanged", this, true);
+
+    BrowserUI.popPopup(this);
+  },
+
+  handleEvent: function handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "TapDown":
+        this.target = aEvent.target;
+        this.deltaX = (aEvent.clientX - this.target.left);
+        this.deltaY = (aEvent.clientY - this.target.top);
+        window.addEventListener("TapMove", this, true);
+        break;
+      case "TapUp":
+        window.removeEventListener("TapMove", this, true);
+        this.target = null;
+        this.deltaX = -1;
+        this.deltaY = -1;
+        break;
+      case "TapMove":
+        if (this.target) {
+          this.target.left = aEvent.clientX - this.deltaX;
+          this.target.top = aEvent.clientY - this.deltaY;
+          let rect = this.target.getBoundingClientRect();
+          let data = this.target == this._start ? { x: rect.right, y: rect.top, type: "start" } : { x: rect.left, y: rect.top, type: "end" };
+          let pos = this.popupState.target.transformClientToBrowser(data.x || 0, data.y || 0);
+          let json = {
+            type: data.type,
+            x: pos.x,
+            y: pos.y
+          };
+          this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionMove", json);
+        }
+        break;
+      case "resize":
+      case "keypress":
+      case "URLChanged":
+      case "SizeChanged":
+      case "ZoomChanged":
+        this.hide();
+        break;
+    }
+  },
+
+  receiveMessage: function sh_receiveMessage(aMessage) {
+    let json = aMessage.json;
+    switch (aMessage.name) {
+      case "Browser:SelectionRange": {
+        let pos = this.popupState.target.transformBrowserToClient(json.start.x || 0, json.start.y || 0);
+        this._start.left = pos.x - 32;
+        this._start.top = pos.y + this.deltaY;
+        this._start.hidden = false;
+
+        pos = this.popupState.target.transformBrowserToClient(json.end.x || 0, json.end.y || 0);
+        this._end.left = pos.x;
+        this._end.top = pos.y;
+        this._end.hidden = false;
+        break;
+      }
+
+      case "Browser:SelectionCopied": {
+        messageManager.removeMessageListener("Browser:SelectionCopied", this);
+        if (json.succeeded) {
+          let toaster = Cc["@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService);
+          toaster.showAlertNotification(null, Strings.browser.GetStringFromName("selectionHelper.textCopied"), "", false, "", null);
+        }
+        break;
+      }
+    }
+  }
+};
+
 var BadgeHandlers = {
   _handlers: [
     {
       _lastUpdate: 0,
       _lastCount: 0,
       url: "https://mail.google.com/mail",
       updateBadge: function(aBadge) {
         // Use the cache if possible
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -908,16 +908,24 @@ var ContextHandler = {
 
           let clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
           let flavors = ["text/unicode"];
           let hasData = clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
 
           if (hasData && !elem.readOnly)
             state.types.push("paste");
           break;
+        } else if (elem instanceof Ci.nsIDOMHTMLParagraphElement ||
+                   elem instanceof Ci.nsIDOMHTMLDivElement ||
+                   elem instanceof Ci.nsIDOMHTMLLIElement ||
+                   elem instanceof Ci.nsIDOMHTMLPreElement ||
+                   elem instanceof Ci.nsIDOMHTMLHeadingElement ||
+                   elem instanceof Ci.nsIDOMHTMLTableCellElement) {
+          state.types.push("content-text");
+          break;
         }
       }
 
       elem = elem.parentNode;
     }
 
     for (let i = 0; i < this._types.length; i++)
       if (this._types[i].handler(state, popupNode))
@@ -1271,16 +1279,17 @@ var TouchEventHandler = {
 
       case "Browser:MouseMove":
         type = "touchmove";
         break;
     }
 
     if (!this.element)
       return;
+
     let cancelled = !this.sendEvent(type, json, this.element);
     if (type == "touchend")
       this.element = null;
 
     if (this.isCancellable) {
       sendAsyncMessage("Browser:CaptureEvents", { messageId: json.messageId,
                                                   type: type,
                                                   contentMightCaptureMouse: true,
@@ -1306,11 +1315,91 @@ var TouchEventHandler = {
     if (aName == "touchend") {
       let empty = content.document.createTouchList();
       evt.initTouchEvent(aName, true, true, content, 0, true, true, true, true, empty, empty, touches);      
     } else {
       evt.initTouchEvent(aName, true, true, content, 0, true, true, true, true, touches, touches, touches);
     }
     return aElement.dispatchEvent(evt);
   }
-}
+};
 
 TouchEventHandler.init();
+
+var SelectionHandler = {
+  cache: {},
+  
+  init: function() {
+    addMessageListener("Browser:SelectionStart", this);
+    addMessageListener("Browser:SelectionEnd", this);
+    addMessageListener("Browser:SelectionMove", this);
+  },
+
+  receiveMessage: function(aMessage) {
+    let scrollOffset = ContentScroll.getScrollOffset(content);
+    let utils = Util.getWindowUtils(content);
+    let json = aMessage.json;
+
+    switch (aMessage.name) {
+      case "Browser:SelectionStart": {
+        // Position the caret using a fake mouse click
+        utils.sendMouseEventToWindow("mousedown", json.x - scrollOffset.x, json.y - scrollOffset.y, 0, 1, 0, true);
+        utils.sendMouseEventToWindow("mouseup", json.x - scrollOffset.x, json.y - scrollOffset.y, 0, 1, 0, true);
+
+        // Select the word nearest the caret
+        let selcon = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).QueryInterface(Ci.nsISelectionController);
+        selcon.wordMove(false, false);
+        selcon.wordMove(true, true);
+
+        // Find the selected text rect and send it back so the handles can position correctly
+        let selection = content.getSelection();
+        let range = selection.getRangeAt(0).QueryInterface(Ci.nsIDOMNSRange);
+
+        this.cache = { start: {}, end: {} };
+        let rects = range.getClientRects();
+        for (let i=0; i<rects.length; i++) {
+          if (i == 0) {
+            this.cache.start.x = rects[i].left + scrollOffset.x;
+            this.cache.start.y = rects[i].bottom + scrollOffset.y;
+          }
+          this.cache.end.x = rects[i].right + scrollOffset.x;
+          this.cache.end.y = rects[i].bottom + scrollOffset.y;
+        }
+
+        sendAsyncMessage("Browser:SelectionRange", this.cache);
+        break;
+      }
+
+      case "Browser:SelectionEnd": {
+        let selection = content.getSelection();
+        let str = selection.toString();
+        selection.collapseToStart();
+        if (str.length) {
+          let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+          clipboard.copyString(str);
+          sendAsyncMessage("Browser:SelectionCopied", { succeeded: true });
+        } else {
+          sendAsyncMessage("Browser:SelectionCopied", { succeeded: false });
+        }
+        break;
+      }
+
+      case "Browser:SelectionMove":
+        if (json.type == "end") {
+          this.cache.end.x = json.x - scrollOffset.x;
+          this.cache.end.y = json.y - scrollOffset.y;
+          utils.sendMouseEventToWindow("mousedown", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
+          utils.sendMouseEventToWindow("mouseup", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
+        } else {
+          this.cache.start.x = json.x - scrollOffset.x;
+          this.cache.start.y = json.y - scrollOffset.y;
+          utils.sendMouseEventToWindow("mousedown", this.cache.start.x, this.cache.start.y, 0, 1, 0, true);
+          // Don't cause a click. A mousedown is enough to move the caret
+          //utils.sendMouseEventToWindow("mouseup", this.cache.start.x, this.cache.start.y, 0, 1, 0, true);
+          utils.sendMouseEventToWindow("mousedown", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
+          utils.sendMouseEventToWindow("mouseup", this.cache.end.x, this.cache.end.y, 0, 1, Ci.nsIDOMNSEvent.SHIFT_MASK, true);
+        }
+        break;
+    }
+  }
+};
+
+SelectionHandler.init();
--- a/mobile/locales/en-US/chrome/browser.properties
+++ b/mobile/locales/en-US/chrome/browser.properties
@@ -230,8 +230,11 @@ clearPrivateData.message=Delete your bro
 browser.menu.showCharacterEncoding=false
 
 # LOCALIZATION NOTE (intl.charsetmenu.browser.static): Set to a series of comma separated
 # values for charsets that the user can select from in the Character Encoding menu.
 intl.charsetmenu.browser.static=iso-8859-1,utf-8,x-gbk,big5,iso-2022-jp,shift_jis,euc-jp
 
 #Application Menu
 appMenu.more=More
+
+#Text Selection
+selectionHelper.textCopied=Text copied to clipboard
--- a/mobile/themes/core/browser.css
+++ b/mobile/themes/core/browser.css
@@ -1526,8 +1526,25 @@ setting {
 @-moz-keyframes sidebardiscoveryrtl {
   from { -moz-transform: translateX(0); }
   10% { -moz-transform: translateX(-moz-calc(-121px - @border_width_large@ - 2*@padding_normal@)); }
   45% { -moz-transform: translateX(-moz-calc(-121px - @border_width_large@ - 2*@padding_normal@)); }
   55% { -moz-transform: translateX(@sidebar_width_minimum@); }
   90% { -moz-transform: translateX(@sidebar_width_minimum@); }
   to { -moz-transform: translateX(0); }
 }
+
+#selectionhandle-start,
+#selectionhandle-end {
+  min-width: 35px !important;
+  width: 35px !important;
+  padding: 0 !important;
+  margin: 0 !important;
+}
+
+#selectionhandle-start {
+  list-style-image: url("chrome://browser/skin/images/handle-start.png");
+}
+
+#selectionhandle-end {
+  list-style-image: url("chrome://browser/skin/images/handle-end.png");
+}
+
--- a/mobile/themes/core/gingerbread/browser.css
+++ b/mobile/themes/core/gingerbread/browser.css
@@ -1492,8 +1492,25 @@ setting {
 @-moz-keyframes sidebardiscovery {
   from { -moz-transform: translateX(0); }
   10% { -moz-transform: translateX(-moz-calc(121px + @border_width_large@ + 2*@padding_normal@)); }
   45% { -moz-transform: translateX(-moz-calc(121px + @border_width_large@ + 2*@padding_normal@)); }
   55% { -moz-transform: translateX(-@sidebar_width_minimum@); }
   90% { -moz-transform: translateX(-@sidebar_width_minimum@); }
   to { -moz-transform: translateX(0); }
 }
+
+#selectionhandle-start,
+#selectionhandle-end {
+  min-width: 35px !important;
+  width: 35px !important;
+  padding: 0 !important;
+  margin: 0 !important;
+}
+
+#selectionhandle-start {
+  list-style-image: url("chrome://browser/skin/images/handle-start.png");
+}
+
+#selectionhandle-end {
+  list-style-image: url("chrome://browser/skin/images/handle-end.png");
+}
+
new file mode 100644
index 0000000000000000000000000000000000000000..d2720e70ec28006a4e881c42f61fc040460f7156
GIT binary patch
literal 1641
zc$@)g2A27WP)<h;3K|Lk000e1NJLTq001KZ002M;1^@s6kh#vS0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU&3rR#lRCwCVSzBxrWfcDZnb})+ySv>A
z7L=yd0t%w2Eyf6?NPudHpq7wGjHnL;Bqs8p5MHo$y9RyZQGLW065|7rgrq<sHa1Z%
zR=Kp4mO?9+wtK%{e$ScC;_P;3cW3re{^WEn|ID6mzH`2FX8zJchYqfCxxOAGD#xx}
z?~zi*@#8<vIma0c#(tNpx05^qgY)^kmSi${5&-R=cQ9_Zo6OA2@X=`O7yvrf-Tfs$
zZ(u^95J5n0_Y4V#!~1nQ-N~MwuPpNdCY4H&csx$LUN164g27-XrCh($+0$5cF!%=n
zB@zkZ_xs88^fU<s{Hqva15T&&<*I|h0nW*-8&`b5jK~lGYe=P%=X!gcomB^e<Bhha
zp^<Bsy#R<IAPAiW7^|AnGQjNYY`eQ>`<lV=p+R@R?@M7~dpw?;O5=H+P)aKY21ol#
zOFf4_+IVT|&d`)+W-1O~aw@H+riO4FS5`n=u9M4~8bZ!}TQ5eY#wPAfjz{Jy&DrR1
zIEck!A%Y;34G^a^vCvT)KYF0+s)hLPOpFZ&<w}Ee9{?b09FBSs5HMC2KwQ2_v&oR`
z{q*hY&puHzDpwjX1xibuGn;F=>g(%?&1NHdy}o2XT%ikij$;Sj-!zt1S|AW$vlZK8
zf?|2XXfz(ItE(e+yPd$km`7TNQ0#@Mv_%W;k?(#USh1*asSTLnp$;J{E`!0a902VB
zg~c$xB9Vw#NU?TR1Wa0Kk2`8FxqkTNnOMV9kAXuybbJJ~+h{a3!*&}m<3&K&^V9-T
z%_%DF=w~|y6TZpGyLZOIKA%s_qfjWg9+dX0)oRtOG))0UrG0at>&gOCZ2b1k>ySnP
z5m36PX;qr$Y%>}3sqTH-hFhPukI0n<OlejcS88q8yK}?sch)YwI(q#tkCM{Ricy+S
zj>`GghSd)iEwDw8o#<cLu%yMFtu&DR9*#q$c^)dQBFv;m+jGsn!yj(EeE-%z_onav
z8^fN4VS}Ew8zg_)X0tjlY-Y1rY_b&vqog!!A&^g~G)U~)mbB6;4yJ(8S{O?W05j_m
zP*uQWD{XA(Z&7K;kk{*TK#@ERuOGW}ww{$MR~$*dC~aK5c!;0`GYl`I3RXCec$NsA
z|6|Z>GTRIQ1aIP@7;fLZd6TZM@5~uRFt5MZ2w*U|z;H`BX)8a@VMKmRGv<~zFAg;>
zw0iq~yHLvudOd@a*dhQ>`<5-6n>so=PMtk_HaRCuDb4_<CybXFr)0YS_xbd(E^B_)
zTAyF$JF<7{#p#I~cOTrFjAHM?dX!cgygfSRFw@bxOk{>pK{D9q1j&FI*b*v8EXnt!
zInj98{Omsdj?HRKw70F8I{*6xy~iIm>hwk(Hd*l91T4L~Zr$2b8AT7DBIjV~4V!yX
z%pN8?%zX_Gn_Zu9eg5&)Z@>R3IyySM<jJNM3l>0=$<%^9jhid39QH7o<+;Jjo?MVI
zs8bFZgp)Uz*cYGd8hQ1F=HZ*e*F>ewwh*@?lNrTkMitqjIT$GB{tvp|oN(CtV}Fjg
zU%_zUCw6hnh?XU5i7h4?SHNga+q&ts2kEa_QDK=6Kvg|m22G4;yjP)XGl?jw6q9_;
z^7>=y!K9gCYFR0GiWc=tLjG1&%oEW(r<f{^3giqeje4f;{;Zs#DtMrUoltd*W--GR
zbimYvjj3Z&C@w5iTbQUid8?>1MFLd!MO&!OuoB8vcD*U(TbpLT!R5&nriDFLO3qN_
zAJMcFLyMPo{sb-|8MH||U+vJiD#bJ>uu_aEq2(h$>$f)5s0(eBl~8BY_+c7yMpH0~
zQ5r2rn97_Km0m0==Xj*R&>cIvSh<$lHRn5g8Q`aiFg|`87jwk_#Ld^hC0bn35^%X|
zz6Y{`I50L=FkA(w2de|C1=CjnLk!Fh<_4Pvn?Y&g(j%^q#H9<ORU#xR`HM^bQwSy@
zO+>&#GSrpKXeRjxNq*6PLXeQ*iJ%OKt{SN#^NZWKAW;;SU0|49N2~Zq$|8dPlk!mT
n5+g#DieXAhk#x?G{{$ES#fv7Tc(3E@00000NkvXXu0mjf-d73w
new file mode 100644
index 0000000000000000000000000000000000000000..8abd627f0df7052b03a4ccb7fd3615c954fda82a
GIT binary patch
literal 1526
zc$@+D1qu3zP)<h;3K|Lk000e1NJLTq001KZ002M;1^@s6kh#vS0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%m`OxIRCwCVneS^`RT#(5NlufdN!m2(
zC>2H>-4vxm2VG?{Yn_8SSm}%Y0E;k{F=Ry%_QIbP?L^Qw!d~sIZwG@xLGTX{e5v+#
z`YUO>wo9Aj-kYDzkK^;CH{DHgliZu!q&sjZy~#b#=RVK(`{bO262+L9_<%HOP)Z47
zj5r*Q?1cY&7&(D~AQp>-IGs+?*49RBHrrq#arnU0Xb_XhM66aTak*Tiz1@u<lj(GN
zMid1HYh*T*AdnSvQ&ZD#kda9MA;DmfuttJF7IGjCv)TM07K;rEf<Q!3B++QJ{xApv
zfr;I2&*}sIo~G0^6NyB6LZJ`|hr=Y5N|8(^Q(qXRV-TdJ#ooht=Ff0g>Oc_82IwUV
zlGRha9fJ^Vw;MAu2q1q+lGG6h1PEk?cvt2{&iPYuH_9<kiW$&{13@q&tBd~#@Aq~s
z=iOHieKa>WXR`q?W~<e54;*t3%3?;4uq0-@OaBIL4PX3ke6V*{c|SASF)b}E+0qm`
z2FgQ(;6YOK`-0zmd}a3Ku8wHo8q7$cA_$hoZg-r9%XyCR?{3A@(%zGwZVxY<ayyf1
z>o6lgpm}Aj>vFXNknJLf@(}<WJLa)>CE{*Q?M&Sn^Vn_Hbg8wN5sqnXZOxVjv@!x#
z{T-T^4NC(hBEHoH@qCxd_xZ#Xui0ec<<?`hIwl7KP+umK$)5#5B-q{|BYeX%D-K;c
z=P4AbVK6zNqGM#CTA)n#6N$u)Tx*wvP<qGb5pE6l&A)x&Y)I?ctX9WBYkve;csJ*m
z;QroWXm@S@+fS}7syW6Gm}1A+!7+3u8a;9hcGSIXZ_Ioc-23H=(WO$3sTroUW1fD-
zY;SnNoo#gctI3g7o7Ix3Mu*HO$Ml_he*M$&fz4Xn$+S2IArMB*F}{^KVf39>mxcyj
z_t)C9SY^k!0K{nly{I~7a^%u%-}y755uKcs`mj?7)eT?|p&b3Fk6In087AkLBcZx*
zGt>>2{{W|S8|4_yFz6V?@u44fU=IPnopa3A`f~V1m#HSlXof)u*)ca@Gjv}RC5s@0
zNIV`VUeBEP<}0Uac8q2i>>(CQ(+{v2nvx`upje}kNXEB1FN_VmwmdZOMuB5^ePE`i
zf6O`tvT*DeArwgMKi&{N8}Fa%?KvazqTpPGsTh!!1n0#tckfPT9Rs@P&wa)m_;(YM
zf6M><?UBW<&KKgm=;ngOIVa__RtR1U$EK#f>xR~z@0iD%F)NAh{QC9HWml^`m630z
z$7}HNK??$M#cNdceKHJW;cu|C8{0ZMW^-dL{8DG@=AD}#u0h#2&wD6!2&w{5Dto+4
zIT@UB-Pm97Ftxt2BwoI7cJ-sNYuoun=XupvtvoF!O>IJFm<{YNIELTrUATVrEq^Hq
zs;y5o1*6(v@I;3bvvd9GWxwX9qspt5f^q$Q28gPBI$j|j)epnv118n|WYieVsAY!h
z%}T{fRMScYTL@DXPejXH;<|EFK{K?r)N_6J7ifm+$OEk$gzC#^L1s)918_ZM<N7km
zw3UTcS0<{@-a7h>M!*`jI4XUH*U+{K`wg!i<|s2%n^t%=+2gfnhL&E0R#OP7-P+{^
zZ0vqlgSE@`4q9DIUbTQr)tFX`qU#x$)|HGdQf19hTBkU?3e8ki3@uKhMwH25QB?W{
zl{`b^nS!CCqaz%TDwNU_gTT{CGMOaI=iAI4Y4NAIlNz}%s2HBFPkLZhkOOPe3eyeq
zIhgG*oiHs8U^s*+!3@GYfEhq*<ChHhvrAHbL!v<mNyzb%a{LU!q~sUkFry0UdMcXB
zF`{x@jGtmM8D5AeK$;pM6-8Y9n`Cl|Vv09dVVWBNlU7s&<ENFSFv}TX4QhrNSt2>j
ckpBf30KJ*??d0`3ApigX07*qoM6N<$f<DvKF8}}l
new file mode 100644
index 0000000000000000000000000000000000000000..d2720e70ec28006a4e881c42f61fc040460f7156
GIT binary patch
literal 1641
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
new file mode 100644
index 0000000000000000000000000000000000000000..8abd627f0df7052b03a4ccb7fd3615c954fda82a
GIT binary patch
literal 1526
zc$@+D1qu3zP)<h;3K|Lk000e1NJLTq001KZ002M;1^@s6kh#vS0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%m`OxIRCwCVneS^`RT#(5NlufdN!m2(
zC>2H>-4vxm2VG?{Yn_8SSm}%Y0E;k{F=Ry%_QIbP?L^Qw!d~sIZwG@xLGTX{e5v+#
z`YUO>wo9Aj-kYDzkK^;CH{DHgliZu!q&sjZy~#b#=RVK(`{bO262+L9_<%HOP)Z47
zj5r*Q?1cY&7&(D~AQp>-IGs+?*49RBHrrq#arnU0Xb_XhM66aTak*Tiz1@u<lj(GN
zMid1HYh*T*AdnSvQ&ZD#kda9MA;DmfuttJF7IGjCv)TM07K;rEf<Q!3B++QJ{xApv
zfr;I2&*}sIo~G0^6NyB6LZJ`|hr=Y5N|8(^Q(qXRV-TdJ#ooht=Ff0g>Oc_82IwUV
zlGRha9fJ^Vw;MAu2q1q+lGG6h1PEk?cvt2{&iPYuH_9<kiW$&{13@q&tBd~#@Aq~s
z=iOHieKa>WXR`q?W~<e54;*t3%3?;4uq0-@OaBIL4PX3ke6V*{c|SASF)b}E+0qm`
z2FgQ(;6YOK`-0zmd}a3Ku8wHo8q7$cA_$hoZg-r9%XyCR?{3A@(%zGwZVxY<ayyf1
z>o6lgpm}Aj>vFXNknJLf@(}<WJLa)>CE{*Q?M&Sn^Vn_Hbg8wN5sqnXZOxVjv@!x#
z{T-T^4NC(hBEHoH@qCxd_xZ#Xui0ec<<?`hIwl7KP+umK$)5#5B-q{|BYeX%D-K;c
z=P4AbVK6zNqGM#CTA)n#6N$u)Tx*wvP<qGb5pE6l&A)x&Y)I?ctX9WBYkve;csJ*m
z;QroWXm@S@+fS}7syW6Gm}1A+!7+3u8a;9hcGSIXZ_Ioc-23H=(WO$3sTroUW1fD-
zY;SnNoo#gctI3g7o7Ix3Mu*HO$Ml_he*M$&fz4Xn$+S2IArMB*F}{^KVf39>mxcyj
z_t)C9SY^k!0K{nly{I~7a^%u%-}y755uKcs`mj?7)eT?|p&b3Fk6In087AkLBcZx*
zGt>>2{{W|S8|4_yFz6V?@u44fU=IPnopa3A`f~V1m#HSlXof)u*)ca@Gjv}RC5s@0
zNIV`VUeBEP<}0Uac8q2i>>(CQ(+{v2nvx`upje}kNXEB1FN_VmwmdZOMuB5^ePE`i
zf6O`tvT*DeArwgMKi&{N8}Fa%?KvazqTpPGsTh!!1n0#tckfPT9Rs@P&wa)m_;(YM
zf6M><?UBW<&KKgm=;ngOIVa__RtR1U$EK#f>xR~z@0iD%F)NAh{QC9HWml^`m630z
z$7}HNK??$M#cNdceKHJW;cu|C8{0ZMW^-dL{8DG@=AD}#u0h#2&wD6!2&w{5Dto+4
zIT@UB-Pm97Ftxt2BwoI7cJ-sNYuoun=XupvtvoF!O>IJFm<{YNIELTrUATVrEq^Hq
zs;y5o1*6(v@I;3bvvd9GWxwX9qspt5f^q$Q28gPBI$j|j)epnv118n|WYieVsAY!h
z%}T{fRMScYTL@DXPejXH;<|EFK{K?r)N_6J7ifm+$OEk$gzC#^L1s)918_ZM<N7km
zw3UTcS0<{@-a7h>M!*`jI4XUH*U+{K`wg!i<|s2%n^t%=+2gfnhL&E0R#OP7-P+{^
zZ0vqlgSE@`4q9DIUbTQr)tFX`qU#x$)|HGdQf19hTBkU?3e8ki3@uKhMwH25QB?W{
zl{`b^nS!CCqaz%TDwNU_gTT{CGMOaI=iAI4Y4NAIlNz}%s2HBFPkLZhkOOPe3eyeq
zIhgG*oiHs8U^s*+!3@GYfEhq*<ChHhvrAHbL!v<mNyzb%a{LU!q~sUkFry0UdMcXB
zF`{x@jGtmM8D5AeK$;pM6-8Y9n`Cl|Vv09dVVWBNlU7s&<ENFSFv}TX4QhrNSt2>j
ckpBf30KJ*??d0`3ApigX07*qoM6N<$f<DvKF8}}l
--- a/mobile/themes/core/jar.mn
+++ b/mobile/themes/core/jar.mn
@@ -117,16 +117,18 @@ chrome.jar:
   skin/images/autocomplete-desktop-hdpi.png      (images/autocomplete-desktop-hdpi.png)
   skin/images/autocomplete-bookmarked-hdpi.png   (images/autocomplete-bookmarked-hdpi.png)
   skin/images/autocomplete-search-hdpi.png  (images/autocomplete-search-hdpi.png)
   skin/images/play-hdpi.png                 (images/play-hdpi.png)
   skin/images/pause-hdpi.png                (images/pause-hdpi.png)
   skin/images/mute-hdpi.png                 (images/mute-hdpi.png)
   skin/images/unmute-hdpi.png               (images/unmute-hdpi.png)
   skin/images/scrubber-hdpi.png             (images/scrubber-hdpi.png)
+  skin/images/handle-start.png              (images/handle-start.png)
+  skin/images/handle-end.png                (images/handle-end.png)
 
 chrome.jar:
 % skin browser classic/1.0 %skin/gingerbread/ os=Android osversion=2.3 osversion=2.3.3 osversion=2.3.4
 % skin browser gingerbread/1.0 %skin/gingerbread/
   skin/gingerbread/aboutCertError.css                   (aboutCertError.css)
   skin/gingerbread/aboutPage.css                        (aboutPage.css)
   skin/gingerbread/about.css                            (about.css)
   skin/gingerbread/aboutHome.css                        (aboutHome.css)
@@ -235,16 +237,18 @@ chrome.jar:
   skin/gingerbread/images/autocomplete-desktop-hdpi.png      (gingerbread/images/autocomplete-desktop-hdpi.png)
   skin/gingerbread/images/autocomplete-bookmarked-hdpi.png   (gingerbread/images/autocomplete-bookmarked-hdpi.png)
   skin/gingerbread/images/autocomplete-search-hdpi.png  (gingerbread/images/autocomplete-search-hdpi.png)
   skin/gingerbread/images/play-hdpi.png                 (gingerbread/images/play-hdpi.png)
   skin/gingerbread/images/pause-hdpi.png                (gingerbread/images/pause-hdpi.png)
   skin/gingerbread/images/mute-hdpi.png                 (gingerbread/images/mute-hdpi.png)
   skin/gingerbread/images/unmute-hdpi.png               (gingerbread/images/unmute-hdpi.png)
   skin/gingerbread/images/scrubber-hdpi.png             (gingerbread/images/scrubber-hdpi.png)
+  skin/gingerbread/images/handle-start.png              (gingerbread/images/handle-start.png)
+  skin/gingerbread/images/handle-end.png                (gingerbread/images/handle-end.png)
 
 chrome.jar:
 % skin browser classic/1.0 %skin/honeycomb/ os=Android osversion>=3.0
 % skin browser honeycomb/1.0 %skin/honeycomb/
   skin/honeycomb/aboutCertError.css                   (aboutCertError.css)
   skin/honeycomb/aboutPage.css                        (aboutPage.css)
   skin/honeycomb/about.css                            (about.css)
   skin/honeycomb/aboutHome.css                        (aboutHome.css)
@@ -355,9 +359,10 @@ chrome.jar:
   skin/honeycomb/images/autocomplete-desktop-hdpi.png      (honeycomb/images/autocomplete-desktop-hdpi.png)
   skin/honeycomb/images/autocomplete-bookmarked-hdpi.png   (honeycomb/images/autocomplete-bookmarked-hdpi.png)
   skin/honeycomb/images/autocomplete-search-hdpi.png  (honeycomb/images/autocomplete-search-hdpi.png)
   skin/honeycomb/images/play-hdpi.png                 (honeycomb/images/play-hdpi.png)
   skin/honeycomb/images/pause-hdpi.png                (honeycomb/images/pause-hdpi.png)
   skin/honeycomb/images/mute-hdpi.png                 (honeycomb/images/mute-hdpi.png)
   skin/honeycomb/images/unmute-hdpi.png               (honeycomb/images/unmute-hdpi.png)
   skin/honeycomb/images/scrubber-hdpi.png             (honeycomb/images/scrubber-hdpi.png)
-
+  skin/honeycomb/images/handle-start.png              (images/handle-start.png)
+  skin/honeycomb/images/handle-end.png                (images/handle-end.png)