Biz & IT —

How to build a desktop WYSIWYG editor with WebKit and HTML 5

Web technologies are increasingly being used on the desktop to bring richer …

A WYSIWYG editor built with Python, GTK+, and WebKit
A WYSIWYG editor built with Python, GTK+, and WebKit

Software developers are increasingly using Web technologies to build desktop applications. This is because modern HTML rendering engines and emerging standards provide a profoundly powerful foundation for rapid development, rich presentation, and deep Web integration.

Apple's open source WebKit renderer has become the basis for several cross-platform application runtime environments that are designed to empower this trend, including Adobe AIR and Appcelerator's Titanium framework. WebKit can also be used alongside native platform user interface toolkits to build software that delivers all of the advantages of Web technology but also allows tight integration with the underlying platform. In this article, we will look at how the WebKit HTML renderer can be used with GTK+ to make a lightweight WYSIWYG editor with Python.

The magic of contentEditable

When scientist Tim Berners-Lee created the first Web browser in 1991, it was both a hypertext viewer and an interactive editing tool. The ability to seamlessly modify and produce content was a fundamental part of Sir Tim's original vision for the Internet, but the notion of integrating this functionality directly into the browser never really gained broad traction.

The "contentEditable" property, which was originally devised by Microsoft and is now included in the HTML5 specification, makes it possible for Web developers to specify that certain blocks of HTML content should be treated as editable by the browser. This feature is now widely supported in mainstream browsers and is a big step towards enabling the kind of Web that Tim Berners-Lee imagined.

We recently looked at how this feature is used in Snowy, a note-taking Web application that is affiliated with the Tomboy project. The contentEditable attribute also has potential value on the desktop, as it significantly simplifies the task of building a WYSIWYG editor. We can use this feature that's already baked into WebKit and wrap it with a simple native user interface.

For this example we will use the Python programming language and the GTK+ toolkit. I chose those simply because they are familiar to me, but it's important to note that this same technique could easily adapted to work with other toolkits, programming languages, and environments. You could use Cocoa on Mac OS X, or you could use Qt if you wanted a cross-platform solution. The WebKit APIs are relatively consistent between various ports, so the WebKit-specific functions used here should work in most places.

We will start by creating a GTK+ window and populating it with a WebKit rendering element that displays a simple HTML string. This demonstrates how trivially easy it is to use WebKit in a GTK+ application. It uses the WebKit GTK+ port and the associated bindings.

import gtk, webkit

# Create a GTK+ window
w = gtk.Window()
w.set_title("Example Editor")
# Terminate the program when the window is closed
w.connect("destroy", gtk.main_quit)

# Instantiate the WebKit renderer
editor = webkit.WebView()
# Load an HTML string into the renderer
editor.load_html_string("<p>This is a <b>test</b>", "file:///")

# Add the renderer to the window
w.add(editor)
w.show_all()

# Turn over control to the GTK+ main loop
gtk.main()

This gives us a fully functional rendering element that can be used to display HTML content content or build a simple browser. To make it into an editor, we need to turn on the editing mode. You could do this by adding the contenteditable="true" property to a top-level HTML element, but WebKit also provides a more convenient API method for turning the feature on in an embedded renderer.

editor = webkit.WebView()
editor.set_editable(True)

Yes, it really is that easy. With only one line of code, the HTML renderer becomes an HTML editor. The user will be able to modify the content as if it was displayed in a word processor program. The next step is to provide user interface controls that allow the user to apply formatting to the content. WebKit doesn't have its own APIs for setting formatting, but it's relatively trivial to do so with JavaScript.

On the Web, the JavaScript document.execCommand method is used with contentEditable to implement interactive editors. It can be used to apply formatting to selected text and to insert content. The method takes three parameters. The first is a string that indicates which formatting command to use. The second parameter is boolean that indicates whether the rendering engine should provide its own user interface for handling the operation, such as providing an input dialog to prompt the user for a URL when a link is being created. The third parameter can be used to programmatically provide a value for the operation, like when you want to specify what font to apply to content. For most simple formatting operations, the last two values can be set as "false".

Some WebKit bindings give developers convenient access to DOM methods from native code. This might make it possible to call execCommand on an embedded WebKit renderer in some environments using the programming language with which you are building your program. The Python bindings for the GTK+ WebKit port don't offer full DOM access yet, however, so we have to cheat and use JavaScript.

First, we make a string that specifies the JavaScript code for the document.execCommand call. Then we pass this string into WebKit's execute_script method, which runs JavaScript strings in the context of the renderer. The following example shows how to toggle bold formatting on the selected text in the WebKit renderer when the user clicks a GTK+ button.

editor = webkit.WebView()
editor.load_html_string("<p>This is a <b>test</b>", "file:///")
editor.set_editable(True)

def on_click_bold(b):
  editor.execute_script("document.execCommand('bold', false, false)")

b = gtk.Button("_Bold")
b.connect("clicked", on_click_bold)

There are execCommand commands for standard formatting operations, including bold, italic, and underline. More complex formatting operations, such as setting font and color are supported, too. There are also several for inserting content, such as text, HTML, images, bulleted lists, paragraphs, and links. In addition to formatting and content commands, there are some for actions, including cut/copy/paste and undo/redo.

For a full list of standard commands, you can refer to the relevant section of the HTML 5 specification. The QuirksMode website has a really useful compatibility table that you can use to see which of these are supported in various browsers. You can refer to the Safari column to see which ones are supported in WebKit.

For a more complex example, let's have a look at how to handle image insertion. We will use a GTK+ file dialog to prompt the user for an image and then we will insert it into the content field.

editor = webkit.WebView()
editor.load_html_string("<p>This is a <b>test</b>", "file:///")
editor.set_editable(True)

def on_click_insert_image(b):
  dialog = gtk.FileChooserDialog("Select an image file", w, gtk.FILE_CHOOSER_ACTION_OPEN,
    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))

  if dialog.run() == gtk.RESPONSE_OK:
    fn = dialog.get_filename()
    if os.path.exists(fn):
      editor.execute_script(
        "document.execCommand('insertImage', null, '%s');" % fn)
  dialog.destroy()

b = gtk.Button("Insert _Image")
b.connect("clicked", on_click_insert_image)

Some of WebKit's default behaviors are undesirable for an editor. For example, it will automatically load the target URL when a link is clicked. That makes plenty of sense for a Web browser, but it's not really what we want our HTML editor to do. You can use the navigation-requested signal to trap page load requests and either ignore them or perform a specialized action as needed.

The next step to make a full-fledged editor is to build the user interface. In my prototype, I use a GTK+ ActionGroup to construct the menus and toolbars. This offers a few nifty advantages in this specific context.

import gtk, webkit, os

w = gtk.Window()
w.set_title("Example Editor")
w.connect("destroy", gtk.main_quit)

editor = webkit.WebView()
editor.load_html_string("<p>This is a <b>test</b>", "file:///")
editor.set_editable(True)

def on_action(action):
  editor.execute_script(
    "document.execCommand('%s', false, false);" % action.get_name())

actions = gtk.ActionGroup("Actions")
actions.add_actions([
  ("bold", gtk.STOCK_BOLD, "_Bold", "<ctrl>B", None, on_action),
  ("italic", gtk.STOCK_ITALIC, "_Italic", "<ctrl>I", None, on_action),
  ("underline", gtk.STOCK_UNDERLINE, "_Underline", "<ctrl>U", None, on_action),
])

ui_def = """
<toolbar name="toolbar_format">
  <toolitem action="bold" />
  <toolitem action="italic" />
  <toolitem action="underline" />
</toolbar>
"""

ui = gtk.UIManager()
ui.insert_action_group(actions)
ui.add_ui_from_string(ui_def)

vb = gtk.VBox()
vb.pack_start(ui.get_widget("/toolbar_format"), False)
vb.pack_start(editor, True)

w.add(vb)
w.show_all()

gtk.main()

Channel Ars Technica