Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1109666 - Customize behavior when long-pressing a link #61

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
28CE83DB1A1D1E7C00576538 /* FxA.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 28CE83D01A1D1D5100576538 /* FxA.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
28CE84301A1E571900576538 /* json.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE842F1A1E571900576538 /* json.swift */; };
28E5857A1A69E9DE00ACEB4F /* Storage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 282DA4BA1A699E0400A406E2 /* Storage.framework */; };
9B64C2A81A6451A800473AE3 /* LongPress.js in Resources */ = {isa = PBXBuildFile; fileRef = 9B64C2A71A6451A800473AE3 /* LongPress.js */; };
9B9AE2241A48B9A600AF2C23 /* LongPressGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B9AE2231A48B9A500AF2C23 /* LongPressGestureRecognizer.swift */; };
D301AAEE1A3A55B70078DD1D /* TabTrayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D301AAED1A3A55B70078DD1D /* TabTrayController.swift */; };
D308E4E41A5306F500842685 /* SearchEngines.swift in Sources */ = {isa = PBXBuildFile; fileRef = D308E4E31A5306F500842685 /* SearchEngines.swift */; };
D308E4EC1A530A8B00842685 /* SearchEngines.swift in Sources */ = {isa = PBXBuildFile; fileRef = D308E4E31A5306F500842685 /* SearchEngines.swift */; };
Expand Down Expand Up @@ -369,6 +371,8 @@
28CE83CA1A1D1D5100576538 /* FxA.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = FxA.xcodeproj; path = FxA/FxA.xcodeproj; sourceTree = "<group>"; };
28CE83E81A1D206D00576538 /* Client-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Client-Bridging-Header.h"; sourceTree = "<group>"; };
28CE842F1A1E571900576538 /* json.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = json.swift; path = ThirdParty/json.swift; sourceTree = "<group>"; };
9B64C2A71A6451A800473AE3 /* LongPress.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = LongPress.js; path = JavaScripts/LongPress.js; sourceTree = "<group>"; };
9B9AE2231A48B9A500AF2C23 /* LongPressGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LongPressGestureRecognizer.swift; sourceTree = "<group>"; };
D301AAED1A3A55B70078DD1D /* TabTrayController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabTrayController.swift; sourceTree = "<group>"; };
D308E4E31A5306F500842685 /* SearchEngines.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEngines.swift; sourceTree = "<group>"; };
D314E7F51A37B98700426A76 /* BrowserToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserToolbar.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -608,6 +612,14 @@
name = "Third-Party Source";
sourceTree = "<group>";
};
9B64C29F1A64518000473AE3 /* JavaScripts */ = {
isa = PBXGroup;
children = (
9B64C2A71A6451A800473AE3 /* LongPress.js */,
);
name = JavaScripts;
sourceTree = "<group>";
};
D34DC84C1A16C40C00D49B7B /* Providers */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -638,6 +650,7 @@
D31A0FC61A65D6D000DC8C7E /* SearchSuggestClient.swift */,
D3968F241A38FE8500CEFD3B /* TabManager.swift */,
D301AAED1A3A55B70078DD1D /* TabTrayController.swift */,
9B9AE2231A48B9A500AF2C23 /* LongPressGestureRecognizer.swift */,
D3C744CC1A687D6C004CE85D /* URIFixup.swift */,
);
path = Browser;
Expand Down Expand Up @@ -763,6 +776,7 @@
children = (
E4D6BEB51A092E0300F538BD /* Client.entitlements */,
F84B22431A09165600AAB793 /* Info.plist */,
9B64C29F1A64518000473AE3 /* JavaScripts */,
F84B21EB1A0910F600AAB793 /* Assets */,
F84B21E41A0910F600AAB793 /* Application */,
F84B21F11A0910F600AAB793 /* Frontend */,
Expand Down Expand Up @@ -1203,6 +1217,7 @@
E4D6BEA61A092A4700F538BD /* Login.xcassets in Resources */,
F84B22271A09127C00AAB793 /* TabBar.xcassets in Resources */,
F84B220E1A0910F600AAB793 /* ReaderViewController.xib in Resources */,
9B64C2A81A6451A800473AE3 /* LongPress.js in Resources */,
E4CD9E9A1A68980A00318571 /* ReaderMode.js in Resources */,
F84B22121A0910F600AAB793 /* SettingsViewController.xib in Resources */,
F84B22291A0912C500AAB793 /* Settings.xcassets in Resources */,
Expand Down Expand Up @@ -1294,6 +1309,7 @@
F84B22241A09122500AAB793 /* TabBarViewController.swift in Sources */,
D34F86E71A1AC7DD00331EC4 /* PasswordTextField.swift in Sources */,
28CE83C91A1D1D3200576538 /* TokenServerClient.swift in Sources */,
9B9AE2241A48B9A600AF2C23 /* LongPressGestureRecognizer.swift in Sources */,
28CE83C71A1D1D3200576538 /* Records.swift in Sources */,
F84B22111A0910F600AAB793 /* SettingsViewController.swift in Sources */,
D34DC8531A16C40C00D49B7B /* Profile.swift in Sources */,
Expand Down
53 changes: 52 additions & 1 deletion Client/Frontend/Browser/BrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ extension BrowserViewController: TabManagerDelegate {
}

func didCreateTab(tab: Browser) {
if let longPressGestureRecognizer = LongPressGestureRecognizer(browser: tab) {
tab.webView.addGestureRecognizer(longPressGestureRecognizer)
longPressGestureRecognizer.longPressGestureDelegate = self
tab.addHelper(longPressGestureRecognizer, name: LongPressGestureRecognizer.name())
}

if let readerMode = ReaderMode(browser: tab) {
readerMode.delegate = self
tab.addHelper(readerMode, name: ReaderMode.name())
Expand Down Expand Up @@ -334,4 +340,49 @@ extension BrowserViewController: ReaderModeDelegate, UIPopoverPresentationContro
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
}
}

extension BrowserViewController: LongPressGestureDelegate {
func longPressRecognizer(longPressRecognizer: LongPressGestureRecognizer, didLongPressElements elements: [LongPressElementType: NSURL]) {
var actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertControllerStyle.ActionSheet)
var dialogTitleURL: NSURL?
if let linkURL = elements[LongPressElementType.Link] {
dialogTitleURL = linkURL
var openNewTabAction = UIAlertAction(title: "Open In New Tab", style: UIAlertActionStyle.Default) { (action: UIAlertAction!) in
let request = NSURLRequest(URL: linkURL)
let tab = self.tabManager.addTab(request: request)
}
actionSheetController.addAction(openNewTabAction)

var copyAction = UIAlertAction(title: "Copy", style: UIAlertActionStyle.Default) { (action: UIAlertAction!) -> Void in
var pasteBoard = UIPasteboard.generalPasteboard()
pasteBoard.string = linkURL.absoluteString
}
actionSheetController.addAction(copyAction)
}
if let imageURL = elements[LongPressElementType.Image] {
if dialogTitleURL == nil {
dialogTitleURL = imageURL
}
var saveImageAction = UIAlertAction(title: "Save Image", style: UIAlertActionStyle.Default) { (action: UIAlertAction!) -> Void in
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
var imageData = NSData(contentsOfURL: imageURL)
if imageData != nil {
UIImageWriteToSavedPhotosAlbum(UIImage(data: imageData!), nil, nil, nil)
}
})
}
actionSheetController.addAction(saveImageAction)

var copyAction = UIAlertAction(title: "Copy Image URL", style: UIAlertActionStyle.Default) { (action: UIAlertAction!) -> Void in
var pasteBoard = UIPasteboard.generalPasteboard()
pasteBoard.string = imageURL.absoluteString
}
actionSheetController.addAction(copyAction)
}
actionSheetController.title = dialogTitleURL!.absoluteString
var cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, nil)
actionSheetController.addAction(cancelAction)
self.presentViewController(actionSheetController, animated: true, completion: nil)
}
}
128 changes: 128 additions & 0 deletions Client/Frontend/Browser/LongPressGestureRecognizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import WebKit
import UIKit

enum LongPressElementType {
case Image
case Link
}

protocol LongPressGestureDelegate: class {
func longPressRecognizer(longPressRecognizer: LongPressGestureRecognizer, didLongPressElements elements: [LongPressElementType: NSURL])
}

class LongPressGestureRecognizer: UILongPressGestureRecognizer, UIGestureRecognizerDelegate, BrowserHelper {
private weak var browser: Browser!
weak var longPressGestureDelegate: LongPressGestureDelegate?

override init(target: AnyObject, action: Selector) {
super.init(target: target, action: action)
}

required init?(browser: Browser) {
super.init()
self.browser = browser
delegate = self
self.minimumPressDuration *= 0.9
self.addTarget(self, action: "SELdidLongPress:")

if let path = NSBundle.mainBundle().pathForResource("LongPress", ofType: "js") {
if let source = NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil) {
var userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.AtDocumentStart, forMainFrameOnly: false)
self.browser.webView.configuration.userContentController.addUserScript(userScript)
}
}
}

// MARK: - Gesture Recognizer Delegate Methods
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

// MARK: - Long Press Gesture Handling
func SELdidLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.Began {
//Finding actual touch location in webView
var touchLocation = gestureRecognizer.locationInView(self.browser.webView)
touchLocation.x -= self.browser.webView.scrollView.contentInset.left
touchLocation.y -= self.browser.webView.scrollView.contentInset.top
touchLocation.x /= self.browser.webView.scrollView.zoomScale
touchLocation.y /= self.browser.webView.scrollView.zoomScale

self.browser.webView.evaluateJavaScript("findElementsAtPoint(\(touchLocation.x),\(touchLocation.y))", completionHandler:nil)
}
}

/// Recursively call block on view and its subviews
private func recursiveBlockOnViewAndSubviews(mainView: UIView, block:(view: UIView) -> Void) {
block(view: mainView)
mainView.subviews.map(){ self.recursiveBlockOnViewAndSubviews($0 as UIView, block) }
}

/// Find location in screen corresponding to webview - in case it is zoomed or scrolled
private func rectLocationInWebView(webView:WKWebView,locationRect:CGRect) -> CGRect {
var rect = locationRect
var scale = self.browser.webView.scrollView.zoomScale
rect.origin.x *= scale
rect.origin.y *= scale
rect.size.width *= scale
rect.size.height *= scale
rect.origin.x += self.browser.webView.scrollView.contentInset.left;
rect.origin.y += self.browser.webView.scrollView.contentInset.top;

return rect
}

// MARK: - BrowserHelper Mehods
class func name() -> String {
return "BrowserLongPressGestureRecognizer"
}

func scriptMessageHandlerName() -> String? {
return "longPressMessageHandler"
}

func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
var elementsDict: [String: AnyObject]? = message.body as? [String: AnyObject]
if elementsDict == nil {
return
}

var elements = [LongPressElementType: NSURL]()
if let hrefElement = elementsDict!["hrefElement"] as? [String: String] {
if let hrefStr: String = hrefElement["hrefLink"] {
if let linkURL = NSURL(string: hrefStr) {
elements[LongPressElementType.Link] = linkURL
}
}
}
if let imageElement = elementsDict!["imageElement"] as? [String: String] {
if let imageSrcStr: String = imageElement["imageSrc"] {
if let imageURL = NSURL(string: imageSrcStr) {
elements[LongPressElementType.Image] = imageURL
}
}
}

if elements.count > 0 {
var disableGestures: [UIGestureRecognizer] = []
self.recursiveBlockOnViewAndSubviews(self.browser.webView) { view in
if let gestureRecognizers = view.gestureRecognizers as? [UIGestureRecognizer] {
for g in gestureRecognizers {
if g != self && g.enabled == true {
g.enabled = false
disableGestures.append(g)
}
}
}
}

self.longPressGestureDelegate?.longPressRecognizer(self, didLongPressElements: elements)
disableGestures.map({ $0.enabled = true })
}
}
}
34 changes: 34 additions & 0 deletions Client/JavaScripts/LongPress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

function findImageSrcLinkAtPoint(x, y) {
var imageLink = {};
e = document.elementFromPoint(x, y);
while (e && ! e.src) {
e = e.parentNode;
}
if (e && e.src) {
imageLink["imageSrc"] = e.src
}
return imageLink;
}

function findHrefLinkAtPoint(x, y) {
var hrefLink = {};
e = document.elementFromPoint(x, y);
while (e && ! e.href) {
e = e.parentNode;
}
if (e && e.href) {
hrefLink["hrefLink"] = e.href
}
return hrefLink;
}

function findElementsAtPoint(x, y) {
var jsonResult = {};
jsonResult["hrefElement"] = findHrefLinkAtPoint(x, y);
jsonResult["imageElement"] = findImageSrcLinkAtPoint(x, y);
webkit.messageHandlers.longPressMessageHandler.postMessage(jsonResult);
}