MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


BYOB: Build Your Own Browser, Part 3

by Andrew Anderson
06/04/2004

Editor's note: In part two of this series, Andrew Anderson enhanced his basic WebKit browser with multi-window capabilities. In this third and final installment, he includes a preferences window and content eliminator.

Preferences

The next feature we are going to add to our browser is a preferences window. This will allow users to set options that are applied across all instances of the browser window. We will implement the window so we can set options for both the WebView object and options that we add to the mix, such as the default homepage.

Building the preferences window will take three steps: first we will set up MyDocument.nib to work with the preferences window. Second, we will build and connect a preferences window, and finally, we will add a controller class to handle the "work" of the preferences window.

Altering MyDocument.nib

The first thing we need to do is to setup MyDocument to work with our preferences window. The preferences window will access a WebPreference object that we will make static between the instances of the WebView objects in our MyDocument windows. WebView contains the functionality to share WebPreference objects between its instances if we set the WebView's Preferences ID.

To set the Preferences ID, load MyDocument.nib into Interface Builder, choose the WebView object, and choose Show Info from the Tools pull-down menu to make the info window pop up. Choose the Attributes selection from the list box, and change the Preferences ID value to "BYOB."

Next, set the other default values for the WebView preferences. I prefer to deselect all of the preferences except the "Maintain back/forward list" (this needs to be checked, because it is set on an instance basis, not a shared basis), so that I know what the default state is. Once we get our preferences window fully developed, the values will be loaded whenever we start the program, so the default values can be set to whatever you want. Save the file, and we are done!

The Preferences Window

The next step is to build the actual preferences window. To do this, we need to edit MainMenu.nib in Interface Builder. When MainMenu.nib is loaded in Interface Builder, drag a window object from the Cocoa-Window panel of the controller window to any place on the screen, causing a new empty window to pop up. Once the window is displayed, go to the Instances pane of the control panel and rename the window from "window" to "prefWindow."

The next step is to add the user interface elements to the window. Our goal is to have the user interface window look like this:

As you can see, we are setting four values from this window:

The first three values are set via check boxes. To add these to the window, drag three check boxes (they are the square boxes with the word Switch next to them) from the Cocoa-Controls panel of the controller window to the window and stack them on top of each other, and then change the text on each.

Next, switch to the Cocoa-Text panel and drag the large System Font Text control to the window. Change the text in this to read "Default Homepage," and then drag a square, boxed text input field onto the window.

Finally switch back to the Cocoa-Controls panel and drag two rounded buttons to the window. Place these buttons on the bottom of the page, and change the text to be Apply and Choose as appropriate.

The last step is to connect the new preferences window to the application pull down menus. First double-click "MainMenu" from the "Instances" pane of Interface Builders control panel, which will make the main menu bar for the application pop up. From the "New Application" menu of the main menu bar, Ctrl-drag a line from the "Preferences" menu choice to the "prefWindow" instance in the control panel. Finally in the "Show Info" window that pops up, go to the "Target/Actions" tab of the connections panel, click "makeKeyAndOrderFront" and choose the "connect" button. Now when a user chooses the "Preferences" menu, the new "prefWindow" will be displayed.

Connecting the Pieces

Related Reading

Cocoa in a Nutshell
A Desktop Quick Reference
By Michael Beam, James Duncan Davidson

Once the preferences window has been built, we need to connect the UI pieces to code. To do this, we need to create a new class to act as the controller between the UI and code. From the Classes tab in the control panel, choose NSObject, go to the Classes pull-down menu, and choose Subclass NSObject. A new object named MyObject will appear in the class navigator. Rename this object Controller.

Once Controller has been created, we need to add the appropriate actions and outlets to it. Make sure that the class is still selected in the class navigator, and choose Add Outlet to Controller from the Classes menu.

Create four outlets, named:

After the outlets are created, we need to add an action to handle the Apply button. Start by choosing the Actions tab, and then click the Add button and create an "apply:" action. Once that is done, close the Controller Class Info window. Next, we need to create an instance of Controller so that we can connect the outlets and actions to the instance. Make sure Controller is selected in the class navigator, and then choose Instantiate Controller from the Classes pull-down menu.

The next step is to connect the UI elements to the Controller instance. First, switch to the Instances tab of the control panel window. Then, Ctrl-drag a line from the Controller instance to each of the UI elements on the screen and choose the appropriate outlet for each UI element.

Choose the Apply button on the UI and Ctrl-drag a line to the Controller instance and choose the "apply:" action. Now we need to connect the Close button by choosing the Close button on the UI, Ctrl-dragging to the "prefWindow" instance, and connecting it to the "performClose:" action. Finally, Ctrl-drag a line from the preferences window instance to the Controller instance and connect it to the "delegate" outlet. This last connection makes the Controller the delegate for the preferences window, so we can control what appears on the window in our code.

The last step in the process is to create the files for the Controller class. To do this, switch back to the Classes tab of the MainMenu.nib window and select the Controller class from the class navigator. Select Create Files For Controller from the Classes pull-down menu. A dialog box will pop up, asking where to put the files; make sure that it has the project selected under "Insert into targets:" and click OK. Once the classes have been created, save the work and quit Interface Builder.

Writing the code

Now it's time to add the code to the Controller.h and Controller.m files that Interface Builder created. Interface Builder already added the outlets and actions for the UI when we created the files. We need to add two variables:


  WebPreferences *p;
  MyDocument *md;

and two import statements:


#import <WebKit/WebPreferences.h>
#import <WebKit/WebView.h>

After adding the statements, the file should look like this:


#import <Cocoa/Cocoa.h>
#import <WebKit/WebPreferences.h>
#import <WebKit/WebView.h>

#import "MyDocument.h"

@interface Controller : NSObject
{
  IBOutlet id allowAnimatedImages;
  IBOutlet id autoloadImages;
  IBOutlet id defaultHomepage;
  IBOutlet id javascriptEnabled;
  IBOutlet id ignoreContent;
  
  WebPreferences *p;
  MyDocument *md;
}
- (IBAction)apply:(id)sender;

@end

Now onto Controller.m, the implementation file for the Controller class, where we will add three methods:

The awakeFromNib method is called by the Cocoa runtime environment when the NIB is first loaded. We will use it to set up preference handling and to load preference values from the user database. windowDidBecomeKey is a window delegate call that is called when ever the preferences window becomes the key window (this delegate was set up when we connected the Controller to the preferences window delegate outlet). This method will make sure that the correct values are displayed on the preferences window whenever it is opened or made key. The last method we will deal with is apply, which will set the values that we input in the preferences window into the structures that we save.

The code for awakeFromNib is pretty straightforward:


- (void)awakeFromNib
{
  md = [MyDocument alloc];
  [md init];

  WebView *wv = [WebView alloc];
  [wv init];
  [wv setPreferencesIdentifier:@"BYOB"];

  p = [wv preferences];
  [p setAutosaves:YES];

  NSUserDefaults *defaults;
  defaults = [NSUserDefaults standardUserDefaults];
  NSString * dHomepage = [defaults stringForKey:@"defaultHomepage"];
  if (dHomepage != nil) {
  [md setDefaultHomepage:dHomepage];
  }
  
 }

First, create a new MyDocument and set it to the instance variable md. We need this value so we can access the static defaultHomepage variable of MyDocument. The next step is to create a WebView object. Once we create the WebView object, we set its preference identifier to BYOB -- the same value that we set in MyDocument's WebView control in Interface Builder. Since they have the same value, they will share the same WebPreferences object, so we pull the WebPreferences object from the WebView and save it into the p variable that we created before.

Calling setAutosaves on p with the value YES ensures that the WebPreferences will automatically take care of loading and saving WebView's preference values in the user default database. Unfortunately, since the default homepage is not part of WebView, our code needs to take care of this. The next group of lines takes care of loading the default homepage value and setting it in the MyDocument object. Since the variable in MyDocument is static, we can set it in our instance and it will be shared across all of the instances.

The apply and windowDidBecomeKey methods are inverses of each other; apply sets values into WebPreferences and MyDocument and windowDidBecomeKey looks up values in WebPreferences and MyDocument. One odd thing that happens is that we setAutosaves every time to YES in the WebPreferences object, p. This is not strictly necessary, but we do this to ensure that the preferences are being saved, on the off chance that the value has changed since the last time we accessed it.

Here is the code for apply and windowDidBecomeKey:


- (IBAction)apply:(id)sender
{  
  NSUserDefaults *defaults;
  defaults = [NSUserDefaults standardUserDefaults];

  NSString* dHomepage = [defaultHomepage stringValue];
  [defaults setObject:dHomepage forKey:@"defaultHomepage"];
  [md setDefaultHomepage:dHomepage];

  NSString* iContent = [ignoreContent stringValue];
  [defaults setObject:iContent forKey:@"ignoreContent"];
  [md setIgnoreContent:iContent];

  [p setAutosaves:YES];
  
  if ([autoloadImages state] == NSOffState){
  [p setLoadsImagesAutomatically:NO];
  }
  else {
  [p setLoadsImagesAutomatically:YES];
  }

  if ([allowAnimatedImages state] == NSOffState){
  [p setAllowsAnimatedImageLooping:NO];
  }
  else {
  [p setAllowsAnimatedImageLooping:YES];
  }

  if ([javascriptEnabled state] == NSOffState){
    [p setJavaScriptEnabled:NO];
  }
  else {
  [p setJavaScriptEnabled:YES];
  }
}


- (void)windowDidBecomeKey:(NSNotification *)aNotification
{
  [defaultHomepage setStringValue:[md getDefaultHomepage]];
  [ignoreContent setStringValue:[md getIgnoreContent]];

  [p setAutosaves:YES];
  
  if ([p loadsImagesAutomatically]){
    [autoloadImages setState:NSOnState];
  }
  else {
  [autoloadImages setState:NSOffState];
  }

  if ([p allowsAnimatedImageLooping]){
    [allowAnimatedImages setState:NSOnState];
  }
  else {
    [allowAnimatedImages setState:NSOffState];
  }

  if ([p isJavaScriptEnabled]){
    [javascriptEnabled setState:NSOnState];
  }
  else {
    [javascriptEnabled setState:NSOffState];
  }
}

You probably noticed that we made use of the NSUserDefaults classes in all three methods to save the information for default homepage. NSUserDefaults allows developers to save information into the user's defaults database that can be reused between instance of the program. This is very convenient and makes it so that developers do not need to make awkward preferences files that are lost if they are not in a specific location.

While I don't go into details about using this here, NSUserDefaults is well documented on Apple's developer site. You may also want to check out the defaults command to see what the defaults are for this and other applications.

Once all the code is written and saved, you're done. When you run the code, you should see that preferences first come up with the default values that you set in Interface Builder. But if you change them, quit and run the browser again; the values you entered on the preference pane should be saved. Adding additional preference variables, or other variables that might be interesting for you (for instance, the value for our Content Eliminator) is pretty straightforward. Build up the interface in Interface Builder and then connect the pieces by adding to each of the methods in Controller.

Content Eliminator

The final feature we're going to add to our browser is a content eliminator. Put simply, a content eliminator will prevent any content from being downloaded if its URL contains certain substrings. For instance, let's say you're sick of the number of banner ads that are on the pages you read. You still want to be able to download the pages that the ads are on, but you don't want to see the ads. If the ads come from a site named singlebutton.com, you would add that string to the content eliminator's list of strings to avoid, and it will prevent the content from being downloaded.

This feature can be used to prevent downloading from ad sites, as a child-guard-type system, or can even be reversed, to only allow content from certain URLs to be used in a kiosk-type application. If you are planning to use this in a kiosk-type application, make sure that you are very careful with the way that you check the strings, or else you may end up with people accessing yoursite.porno-site.com instead of yoursite.com.

To add a content eliminator to our browser, we need to use another of WebView's delegates, this time the policyDelegate. The policyDelegate allows an external application to make decisions about content, specifically how it is to be downloaded and how it is to be displayed. There are several methods that this delegate provides for policy decisions that need to be made. The method that we will be using to make policy decisions is:


decidePolicyForNavigationAction:(NSDictionary *)actionInformation 
  request:(NSURLRequest *)request
  frame:(WebFrame *)frame 
  decisionListener:(id<WebPolicyDecisionListener<)listener

When this method is called by WebView, WebView sends it information about the request being made in several objects, and a listener whose job is to report back to WebView what the method has decided to do about the request. When we implement the method, our code needs to comb through the information that is provided about the request and tell the listener what policy decision was made based on that information.

Before we get too far, the first thing that we need for our content eliminator is to tell the WebView what the policyDelegate is going to be. We do this the same way we did with the UIDelegate and the frameDelegate, by adding:


[webView setPolicyDelegate:self];

to the windowControllerDidLoadNIB method.

Once that is added, we need a static list of sites from which we do not want content to be downloaded. To accomplish this, we are going to use a semi-colon delimited static NSString named ignoreContent. Since our NSString is static, we need to declare it in the MyDocuments implementation file, MyDocument.m, as we did our defaultHomepage variable:


static NSString *ignoreContent = @"site1.com; site2.com;site3.com";

Now we need to build a decidePolicyActionForNavigation method that does four things:

The implementation of the method follows these parameters:


- (void)webView:(WebView *)sender decidePolicyForNavigationAction:
  (NSDictionary *)actionInformation request:(NSURLRequest *)request frame:
  (WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
{
  NSString* urlKey = [[actionInformation objectForKey:WebActionOriginalURLKey] host];

  if(ignoreContent == nil){
  [listener use];
  return;
  }
  if (urlKey == nil){
  [listener ignore];
  return;
  }

  NSArray* theList = [ignoreContent componentsSeparatedByString:@";"];
  int count = [theList count];
  while(count > 0 ){
  NSRange range = [urlKey rangeOfString:[theList objectAtIndex:--count]];
  if(range.length >0){
    [listener ignore];
    return;
  }
  }
  
  [listener use];
}

Once the method has been added, test it out and see if you can block some content or even entire sites. While I left it out of the article, if you follow the instructions in the preferences section, the ignoreContent string is one of the variables that can be added to both the user default database and the preferences window. While this is a simple of example of what a policy delegate can do, policy delegates can be used for any number of interesting filtering-type applications

Final Thoughts

As you can see, WebKit provides the custom browser developer with loads of features that can be used in custom browsers, kiosk applications, browsers built in to other applications, or, really, whatever browsing application you can imagine. Hopefully, you will be inspired to create cool applications that really test the bounds of what WebKit can do.

Good luck developing your browser! Drop us a line if you find any interesting links or if you create any interesting WebKit-based projects.

Andrew Anderson is a software developer and consultant in Chicago who's been using and programming on the Mac as long as he can remember.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.