Customize the contextual menu of UIWebView

本文介绍如何为UIWebView禁用默认上下文菜单并实现自定义功能,包括捕获长按手势、转换屏幕坐标到HTML坐标及通过JavaScript查找触摸位置的HTML元素。

When you tap and hold your finger on a link in a UIWebView object for about a second, a contextual menu will open and provides a few choices: copy the URL to the pasteboard, open the link and a button to close the menu again. Unfortunately there’s no API available to add additional menu items to this contextual menu. But if you look at iCab Mobile, you’ll notice that there are a lot of additional menu items here. How is this done?

First of all, you really can’t add additional menu items to the default ones of the standard contextual menu. But you can switch off the contextual menu using a certain CSS property. So the solution would be to switch off the default menu and implement your own from scratch. And to implement your own contextual menu, you have to first catch the tab-and-hold gesture, get the coordinates of the finger on the screen, translate these coordinates into the coordinate system of the web page and finally look for the HTML elements at this location. Based on the HTML elements, you can then decide which menu items to include in the contextual menu, and then create and display the contextual menu.

The first step is to switch off the default contextual menu of UIWebView. This is easy because we only need to set the CSS property “-webkit-touch-callout” to “none” for the body element of the web page. We can do this using JavaScript in the UIWebView delegate method “webViewDidFinishLoad:”…

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
   [webView stringByEvaluatingJavaScriptFromString:@"document.body.style.webkitTouchCallout='none';"];
}

Now, the default Contextual menu won’t open anymore.

The next step is to catch the “tap-and-hold” gesture. If your App only needs to run on iOS 3.2, iOS 4.0 or newer, you can simply use the new UIGestureRecognizer API. But if you still want to address the 1st generation iPod Touch and iPhone devices (where you can install at most iOS 3.1) where these UIGestureRecognizer classes are not available, you need to do a little bit more. I’ll show here a solution which will work with iOS 3.0 as well, so it’s compatible to all iPhone, iPad and iPod Touch devices.

A big problem when you need to catch gestures within UIWebView is that UIWebView internally consists of many nested views, and only the inner ones do respond to events and gestures. Which means, if you subclass UIWebView and overwrite the methods “touchesBegan:withEvent:”, “touchesMoved:withEvent:” etc., you’ll notice that these methods won’t be called. The events are delivered directly to these private inner views which are actually doing all the work. And if you overwrite “hitTest:withEvent:”, which is used by the iOS to find the view to which the events will be delivered, you would be able to get the touch events, but then you would have to deliver the events to these inner views yourself so these can still do their work. And because these inner views are private and the relationships between these views are unknown, it can be extremely dangerous to mess with the events here.

A much easier way would be to subclass UIWindow and overwrite the method “sendEvent:” of the UIWindows class. Here you’ll get all events before they are delivered to the views. And we can deliver the tap-and-hold events using the notification manager to the rest of the app. Each object that is interested in this gesture can listen to this notification.

Recognizing the tap-and-hold gesture is not very complicated. What we need to do is to save the screen coordinates of the finger when the finger first touches the screen. At that time we will also start a timer which fires after about a second. As soon as another finger touches the screen, the finger moves or the touch event is canceled, we invalidate the timer because then it can not be a simple tap-and-hold gesture anymore. If the timer fires, we can be sure that a single finger has touched the screen for about a second without moving and then we’ve recognized the “tap-and-hold” gesture. We post the gesture as notification.

This is the implementation of the UIWindow subclass:

MyWindow.h:

@interface MyWindow : UIWindow
{
   CGPoint    tapLocation;
   NSTimer    *contextualMenuTimer;
}
@end

MyWindow.m:

#import "MyWindow.h"

@implementation MyWindow

- (void)tapAndHoldAction:(NSTimer*)timer
{
   contextualMenuTimer = nil;
   NSDictionary *coord = [NSDictionary dictionaryWithObjectsAndKeys:
             [NSNumber numberWithFloat:tapLocation.x],@"x",
             [NSNumber numberWithFloat:tapLocation.y],@"y",nil];
   [[NSNotificationCenter defaultCenter] postNotificationName:@"TapAndHoldNotification" object:coord];
}

- (void)sendEvent:(UIEvent *)event
{
   NSSet *touches = [event touchesForWindow:self];
   [touches retain];

   [super sendEvent:event];    // Call super to make sure the event is processed as usual

   if ([touches count] == 1) { // We're only interested in one-finger events
      UITouch *touch = [touches anyObject];

      switch ([touch phase]) {
         case UITouchPhaseBegan:  // A finger touched the screen
            tapLocation = [touch locationInView:self];
            [contextualMenuTimer invalidate];
            contextualMenuTimer = [NSTimer scheduledTimerWithTimeInterval:0.8
                        target:self selector:@selector(tapAndHoldAction:)
                        userInfo:nil repeats:NO];
            break;

         case UITouchPhaseEnded:
         case UITouchPhaseMoved:
         case UITouchPhaseCancelled:
            [contextualMenuTimer invalidate];
            contextualMenuTimer = nil;
            break;
      }
   } else {                    // Multiple fingers are touching the screen
      [contextualMenuTimer invalidate];
      contextualMenuTimer = nil;
   }
   [touches release];
}
@end

Some remarks for the UITouchPhaseMoved phase: it can be sometimes useful to allow small movements on the screen. You can add some code to check for the distance the finger has moved and if it is within a certain range, you just don’t abort the timer. This helps users which have difficulties to hold the finger still for about a second.

Another important thing you have to do when the App window is created by a NIB file: you have to change the UIWindow class of the window within the NIB file in Interface Builder to the new subclass MyWindow. This way the window is created with our subclass, which is important.

The next step is to listen for the “TapAndHoldNotification” notification within the UIWebView delegate and when this notification is received, we need to check which HTML element was touched.

When initializing the UIWebView delegate, we need to add the delegate as observer for the notification…

  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextualMenuAction:) name:@"TapAndHoldNotification" object:nil];
}

And here’s the method “contextualMenuAction:”…

- (void)contextualMenuAction:(NSNotification*)notification
{
   CGPoint pt;
   NSDictionary *coord = [notification object];
   pt.x = [[coord objectForKey:@"x"] floatValue];
   pt.y = [[coord objectForKey:@"y"] floatValue];

   // convert point from window to view coordinate system
   pt = [webView convertPoint:pt fromView:nil];

   // convert point from view to HTML coordinate system
   CGPoint offset  = [webView scrollOffset];
   CGSize viewSize = [webView frame].size;
   CGSize windowSize = [webView windowSize];

   CGFloat f = windowSize.width / viewSize.width;
   pt.x = pt.x * f + offset.x;
   pt.y = pt.y * f + offset.y;

   [self openContextualMenuAt:pt];
}

The method “scrollOffset” and “windowSize” are implemented as category for the UIWebView class. “scrollOffset” is required to make sure the coordinates are also correct when the web page was scrolled. The “windowSize” returns the visible width and height of the HTML document from the point of view of the HTML document. So based on the windowsSize of the “HTML window” and the view size of the UIWebView, you can calculate the zoom factor, and the zoom factor is necessary to transform and scale the screen coordinates to the correct HTML coordinates.

Here’s the implementation of “scrollOffset” and “windowSize”…

WebViewAdditions.h:

@interface UIWebView(WebViewAdditions)
- (CGSize)windowSize;
- (CGPoint)scrollOffset;
@end

WebViewAdditions.m:

#import "WebViewAdditions.h"

@implementation UIWebView(WebViewAdditions)

- (CGSize)windowSize
{
   CGSize size;
   size.width = [[self stringByEvaluatingJavaScriptFromString:@"window.innerWidth"] integerValue];
   size.height = [[self stringByEvaluatingJavaScriptFromString:@"window.innerHeight"] integerValue];
   return size;
}

- (CGPoint)scrollOffset
{
   CGPoint pt;
   pt.x = [[self stringByEvaluatingJavaScriptFromString:@"window.pageXOffset"] integerValue];
   pt.y = [[self stringByEvaluatingJavaScriptFromString:@"window.pageYOffset"] integerValue];
   return pt;
}
@end

Finally, we need to implement the method “openContextualMenuAt:” for the UIWebView delegate, which first checks for the HTML elements that are at the touch locations and the creates the contextual menu. Checking for the HTML elements at the touch location must be done via JavaScript…

JSTools.js

function MyAppGetHTMLElementsAtPoint(x,y) {
   var tags = ",";
   var e = document.elementFromPoint(x,y);
   while (e) {
      if (e.tagName) {
         tags += e.tagName + ',';
      }
      e = e.parentNode;
   }
   return tags;
}


This JavaScript function simply collects the tag names of all HTML elements at the touch coordinates and returns the tag names as string list. The JavaScript file must be added as “resource” to your XCode project. It can happen that Xcode treats JavaScript file as normal code and tries to compile and link it instead of adding it to the resources. So make sure the JavaScript file is in the “Copy Bundle Resources” section within the XCode project target and not in the “Compile Sources” or “Link Binaries” section.

- (void)openContextualMenuAt:(CGPoint)pt
{
   // Load the JavaScript code from the Resources and inject it into the web page
   NSString *path = [[NSBundle mainBundle] pathForResource:@"JSTools" ofType:@"js"];
   NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
   [webView stringByEvaluatingJavaScriptFromString: jsCode];

   // get the Tags at the touch location
   NSString *tags = [webView stringByEvaluatingJavaScriptFromString:
            [NSString stringWithFormat:@"MyAppGetHTMLElementsAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];

   // create the UIActionSheet and populate it with buttons related to the tags
   UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"Contextual Menu"
                  delegate:self cancelButtonTitle:@"Cancel"
                  destructiveButtonTitle:nil otherButtonTitles:nil];

   // If a link was touched, add link-related buttons
   if ([tags rangeOfString:@",A,"].location != NSNotFound) {
      [sheet addButtonWithTitle:@"Open Link"];
      [sheet addButtonWithTitle:@"Open Link in Tab"];
      [sheet addButtonWithTitle:@"Download Link"];
   }
   // If an image was touched, add image-related buttons
   if ([tags rangeOfString:@",IMG,"].location != NSNotFound) {
      [sheet addButtonWithTitle:@"Save Picture"];
   }
   // Add buttons which should be always available
   [sheet addButtonWithTitle:@"Save Page as Bookmark"];
   [sheet addButtonWithTitle:@"Open Page in Safari"];

   [sheet showInView:webView];
   [sheet release];
}

This method injects the JavaScript code which looks for the HTML elements at the touch location into the web page and calls this function. The return value will be a string with a comma-separated list of tag names. This string will start and end with a comma so we can simply check for occurrences of a substring “,tagName,” if we want to find out if an element with a certain tag name was touched. In our example, we simple add some buttons if an “A” tag was hit and some other buttons if and “IMG” tag was hit. But what you’re doing is up to you. Also the information that is returned from the JavaScript function (in this example “MyAppGetHTMLElementsAtPoint()”) is up to you. In iCab Mobile it returns the HREF and SRC attributes of A and IMG tags, so the URLS can be directly processed.

The example doesn’t include the UIActionSheet delegate method which is called when you tab on one of the buttons in the contextual menu. But I think you should already know how to handle this. Also a few other details might be missing, but I think you should be able now to implement your own custom contextual menu for UIWebView objects with the information from the blog post.


<think>好的,用户的问题是如何在IDE或软件的Tools菜单中添加一个Import选项。我需要根据提供的引用内容来寻找可能的解决方案。首先,查看用户提供的三个引用,看看是否有相关的信息。 引用[1]中提到,在Eclipse 4.5中,如果现有的工具栏和视图扩展点贡献不起作用,可以通过注入MToolbar并在源代码中构造条目来为e4视图添加工具栏。这说明在Eclipse中,可以通过编程方式修改菜单或工具栏,而不是通过扩展点。这可能适用于Eclipse插件开发的情况。 引用[2]提到了Netbeans的Python插件,但并没有直接涉及如何添加菜单选项的内容,可能不太相关。 引用[3]讨论了导入/导出数据提示的功能,类似于断点的导入导出,但同样没有直接说明如何添加菜单项。 因此,主要的线索来自引用[1],涉及在Eclipse中使用注入的方式添加工具栏条目。假设用户是在基于Eclipse的IDE中进行开发,需要添加一个Import选项到Tools菜单中,可能需要通过扩展点或者编程方式实现。 首先,考虑Eclipse插件开发中的扩展点。通常,Eclipse允许通过plugin.xml声明菜单项。例如,使用org.eclipse.ui.menus扩展点来添加菜单项。如果现有的扩展点不适用(如引用[1]所述的情况),可能需要直接操作菜单的MToolbar或MTrimmedWindow,通过代码动态添加菜单项。 步骤可能包括: 1. 获取当前窗口的MTrimmedWindow实例。 2. 找到Tools菜单的位置,或者创建一个新的菜单项。 3. 创建命令或处理程序,关联到Import选项的动作。 4. 将新创建的菜单项添加到Tools菜单中。 另外,如果用户使用的是其他IDE如Netbeans,可能需要不同的方法,比如通过Action类注册菜单项。但根据引用[2],用户可能更关注Eclipse或PyDev,但引用中没有给出Netbeans的具体方法。 因此,重点应放在Eclipse插件开发的方法上,引用[1]中提到注入MToolbar并构造条目,这可能涉及到在代码中动态添加菜单项,而不是通过扩展点。需要检查Eclipse的API文档,了解如何正确获取和修改菜单结构。 总结可能的解决方案: - 对于Eclipse,使用扩展点或在代码中动态添加菜单项。 - 如果扩展点不可用(如引用[1]的情况),则通过注入服务获取菜单,并编程添加条目。 需要验证这些步骤,并确保代码示例的正确性,同时提醒用户可能需要根据具体IDE或框架调整方法。</think>在IDE或软件中向Tools菜单添加Import选项通常涉及扩展菜单功能。以下是基于不同开发环境的实现方法: ### 一、Eclipse插件开发 **1. 通过扩展点添加(推荐)** 在`plugin.xml`中使用`org.eclipse.ui.menus`扩展点: ```xml <extension point="org.eclipse.ui.menus"> <menuContribution locationURI="menu:org.eclipse.ui.main.menu?after=tools"> <command commandId="com.example.import.command" label="Import Data" tooltip="Import external data"> </command> </menuContribution> </extension> ``` 需配合`org.eclipse.ui.commands`定义命令和处理器[^1]。 **2. 编程式注入(当扩展点不可用时)** 在视图/处理器类中注入`MToolBar`: ```java @Inject public void createToolbar(MToolBar toolbar) { MHandledToolItem importItem = MenuFactory.eINSTANCE.createHandledToolItem(); importItem.setCommandId("com.example.import.command"); toolbar.getChildren().add(importItem); } ``` 需确保上下文正确绑定到`MApplication`[^1]。 ### 二、Netbeans模块开发 通过`Actions`和`Layer.xml`注册: ```xml <folder name="Menu"> <folder name="Tools"> <file name="com-example-ImportAction.instance"/> </folder> </folder> ``` 需创建继承`CookieAction`的`ImportAction`类实现具体逻辑[^2]。 ### 三、通用软件实现 以C# WinForms为例: ```csharp ToolStripMenuItem importItem = new ToolStripMenuItem("Import"); importItem.Click += (sender, e) => { /* 导入逻辑 */ }; toolsMenu.DropDownItems.Add(importItem); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值