Practical JavaFX 2, Part 1: Architecture of a Swing-based notepad

feature
Mar 6, 201220 mins

How will Swing JPad's UI features map to JavaFX 2.0?

With the release of JavaFX 2.0 the question for many Java developers is whether it’s time to start transitioning Swing applications to this new UI paradigm, and what better way to find out than by a hands-on exercise? In this three-part tutorial, Jeff Friesen walks through the process of refactoring an example Swing-based notepad editor application to its JavaFX 2 equivalent.

JavaFX 2 is a user interface library and platform for creating and running modern Java application UIs that feature rich graphics, animation, media, controls, and more. It’s also a toolkit for deploying JavaFX applications, meaning any Java application that features a JavaFX UI. A JavaFX application can be deployed standalone, launched via Java Web Start, or embedded in a web page.

Practical JavaFX 2: Read all three parts

This article serves as a practical introduction to JavaFX 2, and is geared to developers experienced with Swing-based user interface (UI) development. By refactoring a Swing application, you’ll familiarize yourself with some of the most useful features of JavaFX 2. You’ll have the opportunity to try it on for size. And you’ll get some practice in refactoring Swing apps to JavaFX — which could come in handy if JavaFX 2 turns out to be the next-generation Java development platform that Oracle clearly hopes it will.

Oracle has been pushing JavaFX 2 as the next-generation platform for Java, but does that mean it’s time to jump from Swing to JavaFX? Refactoring a Swing demo app to JavaFX is one way to find out.

This first part of the article is an overview of a Swing demo application, a text editor called JPad. Experienced Swing developers can probably give the architecture a glance, then move on to refactoring the app for JavaFX in Part 2 and Part 3. For those less experienced with Swing, Part 1 offers valuable insight into client-side application development, which you’ll probably need in order to follow the refactoring examples.

JavaFX 2 release history

Oracle released JavaFX 2.0 beta in May 2011 and declared it production-ready in October of that same year. The most recent version, JavaFX 2.0.2, was released in December 2011 with some bug fixes and minor features added. Examples in this article are based on JavaFX 2.0.2.

Let’s start with a review of new features in JavaFX 2, some of which you’ll encounter in the refactored JPad architecture.

Big changes in JavaFX 2

JavaFX 2 differs significantly from previous versions, and one of the biggest changes is the deprecation of JavaFX Script. JavaFX developers are free to program in Java code, and may also experiment with JVM scripting languages such as Groovy or Scala. Java code is more verbose than JavaFX Script, but there’s hope that Java 8’s support for lambda expressions in the Java language will bring improvements.

The following JavaFX Script language features have been replaced with Java API equivalents:

  • Java’s builders replace JavaFX Script’s script initialization blocks. A builder is a Java class whose methods are designed so that you can chain together method calls.
  • Change listeners replace replace triggers.
  • Observable lists replace sequences.
  • Single abstract method (SAM) types replace function types. A SAM type is a single-method interface or abstract class — the abstract class’s method is abstract. When Java 8 arrives, we’ll be able to use lambda expressions to more concisely work with SAM types.
  • Properties based on an expanded and improved version of the JavaBeans model have been introduced to support binding. Whereas JavaFX Script enabled binding to any variable, JavaFX 2.0 only allows binding to properties.

Further changes to JavaFX 2 include the following:

  • Experimental menu, toolbar, and tree UI controls introduced in JavaFX 1.3.1 are now standard. JavaFX 2 also features new table and HTML-editor controls along with standard file dialogs. All controls can be skinned via CSS3+.
  • A new web-view component based on WebKit makes it possible to embed a web browser (with JavaScript and Document Object Model support) into Java applications.
  • JavaFX 2 supports FXML, a scriptable, XML-based markup language for constructing UIs. FXML replaces the former JavaFX Script-oriented FXD (JavaFX Data) format.
  • JavaFX application deployment has been improved. You can now create custom preloaders (specialized JavaFX applications extending the javafx.application.Preloader class) to enhance application loading and startup.
  • JavaFX 2’s application architecture has been improved to simplify the organization of applications that will run in multiple deployment contexts. New APIs enable applications to detect and interact with their deployment environments.
  • A new platform architecture based on a hardware-accelerated graphics pipeline (Prism) and windowing toolkit (Glass) improves the performance of JavaFX 2 applications. When combined with Java ME, it should help in migrating JavaFX applications to mobile and tablet devices.

Is JavaFX 2 production ready?

As you read this article, consider the following questions:

  • Is JavaFX 2 a production-ready platform, as Oracle claims?
  • Are there notable advantages to programming in JavaFX versus Swing?

I’ll share my answers to these questions at the end of Part 3. You’ll also be invited to share your viewpoint in the article’s discussion forum.

Introducing JPad

JPad is a Java-based text editor application that’s very similar to Windows XP’s Notepad program. As with Notepad, JPad lets you open a document file, make changes to its text, and save your changes to the currently open file or a new document file. You can also undo changes, interact with the clipboard through cut/copy/paste operations, and perform other tasks. Figure 1 is a screenshot of JPad’s UI.

Figure 1. A screenshot of Swing JPad (click to enlarge)

Figure 1 reveals a simple UI that consists of a menu bar, a scrollable text area, and a status bar. The menu bar displays File, Edit, Format, and Help menus; the text area displays the current document and lets you make changes; and the status bar displays a default message or the help text that’s associated with the currently selected menu. Note that the Cut and Copy functions are disabled until text is selected.

File offers the following menu items:

  • New creates a new empty document and prompts users to save the current document if it has unsaved changes.
  • Open opens an existing file. Users are prompted to save the current document if there are unsaved changes.
  • Save saves the current document to its associated file or a different file.
  • Save As saves the current document to a different file.
  • Exit exits the application. Users are prompted to save the current document if there are unsaved changes.

Edit offers the following menu items:

  • Undo undoes the last edit; disabled when there are no new edits.
  • Cut cuts the selected text to the clipboard; disabled when no text is selected.
  • Copy copies the selected text to the clipboard; disabled when no text is selected.
  • Paste pastes clipboard text over selected text or at the caret position; disabled when the clipboard has no text.
  • Select All selects all of the text area’s text.

Additionally:

  • Word Wrap can be enabled or disabled by checking or unchecking the Format menu item.
  • About JPad is a menu item under Help that lets users obtain information about the application.

The JPad application also lets users specify a file to be opened as a command-line argument. If a user drops a file being dragged onto its text area, JPad will open that file after letting the user save any recent changes.

Setting up a JavaFX 2 development environment

Oracle’s JavaFX website extensively documents the JavaFX architecture. That is also where you’ll find JavaFX development software for your 32-bit or 64-bit Windows or Mac OS X platform. I used the JDK 7u2 with JavaFX SDK integrated software package (which includes the JavaFX 2.0.2 SDK) on a Windows XP platform to develop my JavaFX 2 JPad application.

Compiling and running JPad

In order to explore JPad in your development environment, download its source code and resource file: JPad.java and icon.png. After extracting these files to your current directory, execute the following commands to compile JPad.java and run it. Note that the third command tells JPad to open and display the contents of JPad.java:

javac JPad.java
java JPad
java JPad JPad.java

Architecture of JPad

JPad is implemented as a single JPad top-level class. As with many Swing applications, JPad extends the javax.swing.JFrame class, which makes it a kind of frame window. You can see this in Listing 1.

Listing 1. The JPad class encapsulates the JPad application

// ...
public class JPad extends JFrame
{
   // ...
   public JPad(String[] args)
   {
      // ...
      if (args.length != 0)
         doOpen(new File(args[0]));
   }
   private void doExit()
   {
      // ...
   }
   private void doNew()
   {
      // ...
   }
   private void doOpen()
   {
      doOpen(null);
   }
   private void doOpen(File file)
   {
      // ...
   }
   private boolean doSave()
   {
      // ...
   }
   private boolean doSaveAs()
   {
      // ...
   }
   private String read(File f) throws IOException
   {
      // ...
   }
   private void write(File f, String text) throws IOException
   {
      // ...
   }
   public static void main(final String[] args)
   {
      Runnable r = new Runnable()
      {
         @Override
         public void run()
         {
            new JPad(args);
         }
      };
      EventQueue.invokeLater(r);
   }
}

Listing 1 shows you JPad‘s skeletal framework, which consists of a constructor, several private methods, and the main() entry-point method. (I’ve commented out everything else because it isn’t relevant at this point.)

The main() method instantiates JPad and passes to its constructor the array of command-line arguments on the event-dispatch thread. After creating JPad’s UI, the constructor opens the file identified by the first command-line argument when at least one argument is specified.

Swing UIs and the event-dispatch thread

In the JavaWorld article “Swing threading and the event-dispatch thread” author John Zukowski explains why it’s important to create Swing UIs on the event-dispatch thread.

Creating the UI

JPad‘s constructor creates the JPad UI by leveraging the javax.swing package’s JCheckBoxMenuItem, JLabel, JMenu, JMenuBar, JMenuItem, JScrollPane, and JTextArea classes, as shown in Listing 2.

Listing 2. Swing organizes JPad’s UI into a menu bar and a content pane

// ...
private JTextArea ta;
// ...
private JLabel lbl;
// ...
public JPad(String[] args)
{
   // ...
   JMenuBar mb = new JMenuBar();
   JMenu mFile = new JMenu("File");
   JMenuItem miNew = new JMenuItem("New");
   miNew.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N,
                                               KeyEvent.CTRL_MASK));
   // ...
   mFile.add(miNew);
   // ...
   mb.add(mFile);
   // ...
   JMenu mFormat = new JMenu("Format");
   final JCheckBoxMenuItem cbmiWordWrap = new JCheckBoxMenuItem("Word Wrap");
   // ...
   mFormat.add(cbmiWordWrap);
   // ...
   mb.add(mFormat);
   // ...
   setJMenuBar(mb);
   getContentPane().add(new JScrollPane (ta = new JTextArea()));
   // ...
   getContentPane().add(lbl = new JLabel("JPad 1.0"), BorderLayout.SOUTH);
   setSize(400, 400);
   setTitle(DEFAULT_TITLE);
   // ...
   setVisible(true);
   // ...
}

The constructor first initializes the JFrame superclass and creates/installs the menu system (although I’ve omitted most of this code for brevity). Next, it creates the scrollable text area and status bar, and adds them to the frame window’s content pane. Lastly, it initializes and displays the window.

Why use setTitle(DEFAULT_TITLE);?

Experienced Swing developers might note that I’ve used setTitle(DEFAULT_TITLE); instead of super(DEFAULT_TITLE); to initialize the frame window’s default titlebar text in Listing 2. I wrote the code this way because the JavaFX 2 application will also require setTitle(). For the sake of example, I wanted JPad to be as consistent with JPadFX as possible.

Handling events

Event handling is one of the more involved aspects of Swing development, which makes refactoring a Swing app’s event-handling infrastructure to JavaFX both challenging and rewarding. Here, we’ll look at how JPad handles typical notepad events triggered by the user, namely: action, document, menu, caret, and window events.

Action events

An action event is fired when the user selects a menu item. For example, when the user selects New from the File menu, an action event is sent to New’s registered action listener, as shown in Listing 3.

Listing 3. JPad responds to a New action event

ActionListener al;
al = new ActionListener()
{
   @Override
   public void actionPerformed(ActionEvent ae)
   {
      if (fDirty)
         switch (JOptionPane.showConfirmDialog(JPad.this, SAVE_CHNGS,
                                               TITLE_AYS,
                                               JOptionPane.YES_NO_OPTION))
         {
            case JOptionPane.YES_OPTION: if (doSave()) doNew(); break;
            case JOptionPane.NO_OPTION : doNew();
         }
      else
         doNew();
   }
};
miNew.addActionListener(al);

The New menu item’s action listener first examines fDirty to learn whether or not the current document has changed. If this Boolean field variable is true (meaning that the document has changed), the user is prompted to save changes before a new document replaces the current one.

Document events

A document event is fired when the user makes a change to the text-area component’s document. This component’s registered document listener responds by setting fDirty (which is initially false) to true, as shown in Listing 4.

Listing 4. changedUpdate() is not applicable to document changes

DocumentListener dl;
dl = new DocumentListener()
{
   @Override
   public void changedUpdate(DocumentEvent de)
   {
      // Not needed -- only called when attributes are changed
   }
   @Override
   public void insertUpdate(DocumentEvent de)
   {
      fDirty = true;
   }
   @Override
   public void removeUpdate(DocumentEvent de)
   {
      fDirty = true;
   }
};
ta.getDocument().addDocumentListener(dl);

The document listener declares insertUpdate() and removeUpdate() methods that are called whenever content is inserted into or removed from the Document object.

A menu event is fired when the user selects a menu. The menu’s registered menu listener responds by updating the status bar with menu-specific help text. When the user unselects the menu, the status bar’s content is reset to default text. Listing 5 demonstrates both tasks.

Listing 5. menuSelected() is called each time a menu is selected

MenuListener ml;
ml = new MenuListener()
{
   @Override
   public void menuCanceled(MenuEvent me)
   {
   }
   @Override
   public void menuDeselected(MenuEvent me)
   {
      lbl.setText(DEFAULT_STATUS);
   }
   @Override
   public void menuSelected(MenuEvent me)
   {
      lbl.setText("New doc|Open existing doc|Save changes|Exit");
   }
};
mFile.addMenuListener(ml);

The menu listener’s menuCanceled() method is left empty because this method is never called. It appears to be a historical artifact.

Caret events

A caret event is fired when the user moves the caret (text-insertion indicator) within the text area component. The component’s registered caret listener determines whether or not text is selected and enables or disables the Cut and Copy menu items appropriately, as demonstrated in Listing 6.

Listing 6. caretUpdate() is called each time the caret moves

CaretListener cl;
cl = new CaretListener()
{
   @Override
   public void caretUpdate(CaretEvent ce)
   {
      if (ta.getSelectedText() != null)
      {
         miCut.setEnabled(true);
         miCopy.setEnabled(true);
      }
      else
      {
         miCut.setEnabled(false);
         miCopy.setEnabled(false);
      }
   }
};
ta.addCaretListener(cl);

The caret listener’s caretUpdate() method calls JTextArea‘s String getSelectedText() method to determine whether text has been selected, then updates Cut and Copy accordingly.

Window events

Finally, we come to the window event, which is fired whenever the user attempts to close the window or perform another window operation. The frame window’s registered window listener invokes doExit(), as shown in Listing 7. (This listener requires setDefaultCloseOperation() to be called with DO_NOTHING_ON_CLOSE as its argument; otherwise, the window listener will be ignored.)

Listing 7. doExit() closes the frame window

setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter()
{
   @Override
   public void windowClosing(WindowEvent we)
   {
      doExit();
   }
});

doExit() prompts the user to save changes when fDirty is true. Whether or not changes are saved, it lastly invokes dispose() to destroy the frame window and terminate the application. Listing 8 shows this code sequence.

Listing 8. dispose() allows JPad to terminate

private void doExit()
{
   if (fDirty)
      switch (JOptionPane.showConfirmDialog(JPad.this, SAVE_CHNGS,
                                            TITLE_AYS,
                                            JOptionPane.YES_NO_OPTION))
      {
         case JOptionPane.YES_OPTION: if (doSave()) dispose(); break;
         case JOptionPane.NO_OPTION : dispose();
      }
   else
      dispose();
}

Dialog boxes

JPad presents dialog boxes for selecting a file from the filesystem, showing About information, displaying error messages, and prompting for yes/no responses. As Listing 9 shows, JPad’s constructor instantiates javax.swing.JFileChooser to handle file-selection. fc.setAcceptAllFileFilterUsed(false) then removes the default “All Files” filter.

Listing 9. Instantiating and configuring JPad’s file chooser

fc = new JFileChooser(".");
fc.setAcceptAllFileFilterUsed(false);
FileFilter ff = new FileNameExtensionFilter("TXT documents (*.txt)",
                                            "*.txt");
fc.addChoosableFileFilter(ff);
ff = new FileNameExtensionFilter("All Files", "*.*");
fc.addChoosableFileFilter(ff);

The constructor first invokes the JFileChooser(String currentDirectoryPath) constructor with a dot (.) as its argument. The dot signifies that the current directory is to serve as the file chooser’s initially displayed directory.

The constructor then removes the file chooser’s default “All Files” file filter. It then registers a pair of filters with the file chooser, instructing it to accept only files with .txt file extensions, followed by all files (meaning that the extension will be ignored).

A file filter is conveniently described by a javax.swing.filechooser.FileNameExtensionFilter instance, which is registered with the file chooser by invoking JFileChooser‘s void addChoosableFileFilter(FileFilter filter) method.

The file chooser is later used to display an open or save dialog box in the void doOpen(File file) or boolean doSaveAs() method. Listing 10 demonstrates the file chooser being used in the former method.

Listing 10. Displaying an open file chooser

if (file == null)
   if (fc.showOpenDialog(JPad.this) == JFileChooser.APPROVE_OPTION)
      file = fc.getSelectedFile();
if (file == null)
   return;
fc.setCurrentDirectory(file.getParentFile());

fc.setCurrentDirectory(file.getParentFile()) ensures that the next file-chooser activation will set the initial directory to the current one.

About JPad

JPad presents its About dialog box in response to the user selecting “About JPad” from the Help menu. This dialog box is created and displayed by invoking one of JOptionPane‘s showMessageDialog() methods, as demonstrated in Listing 11. (Note that icon is a javax.swing.ImageIcon instance previously initialized via icon = new ImageIcon("icon.png");.)

Listing 11. Informing the user about JPad

JOptionPane.showMessageDialog(JPad.this,
                              "JPad 1.0nby Jeff Friesenn",
                              "JPad",
                              JOptionPane.PLAIN_MESSAGE,
                              icon);

Figure 2 is a screenshot of JPad’s About dialog box.

Figure 2. JPad’s About dialog box

The newline character (n) is used to split the message across multiple lines and to leave an extra blank line for padding. showMessageDialog() is also used to display Alert dialog boxes in response to I/O and other errors. Note, too, that one of JOptionPane‘s showConfirmDialog() methods is used to prompt for yes/no responses (also refer back to Listing 3 and Listing 8).

Accessing the clipboard

Users of JPad can access its clipboard via cut, copy, and paste operations. Much of this support is handled automatically by a Swing JTextArea, which accesses the system clipboard and performs a cut, copy, or paste operation when it detects a specific key combination (e.g., Ctrl+C).

JTextArea also declares methods such as void copy(), which programmatically access the system clipboard. JPad’s menu item listeners for the Edit menu’s Cut, Copy, and Paste menu items execute these methods when an item is selected, as in the case of ta.copy();.

JPad also needs to directly access the system clipboard in order to determine when text is available for pasting. When text isn’t available, Edit’s Paste menu item must be disabled; otherwise, this menu item is enabled.

The constructor executes the code shown in Listing 12 to access the system clipboard and save its reference in a java.awt.datatransfer.Clipboard field variable.

Listing 12. getSystemClipboard() provides access to the system clipboard

cb = Toolkit.getDefaultToolkit().getSystemClipboard();

Edit’s menu listener executes the code shown in Listing 13 in its menuSelected() method.

Listing 13. isDataFlavorAvailable() returns true when a string is on the clipboard

boolean b = cb.isDataFlavorAvailable(DataFlavor.stringFlavor);
miPaste.setEnabled(b);

Each time the Edit menu is selected, Clipboard‘s boolean isDataFlavorAvailable(DataFlavor) method is executed on the system clipboard with DataFlavor.stringFlavor, which describes the text’s content type, also known as a MIME type. If the system clipboard contains text, true returns and the Paste menu item is enabled. Otherwise, there is no text on the system clipboard and Paste is disabled.

Drag and drop support

JPad lets you drag files to and drop them on the text area. Although multiple files can be dragged to and dropped on this UI component, only the first of these files is opened and its content displayed; the other files are ignored. Listing 14 shows how JPad supports drag and drop.

Listing 14. JPad responds to drop target drag and drop events

DropTargetAdapter dta;
dta = new DropTargetAdapter()
{
   @Override
   public void dragOver(DropTargetDragEvent dtde)
   {
      Transferable tr = dtde.getTransferable();
      DataFlavor[] flavors = tr.getTransferDataFlavors();
      for (DataFlavor flavor: flavors)
         if (flavor.isFlavorJavaFileListType())
         {
            dtde.acceptDrag(DnDConstants.ACTION_COPY);
            return;
         }
   }
   @Override
   public void drop(DropTargetDropEvent dtde)
   {
      try
      {
         Transferable tr = dtde.getTransferable();
         DataFlavor[] flavors = tr.getTransferDataFlavors();
         for (DataFlavor flavor: flavors)
            if (flavor.isFlavorJavaFileListType())
            {
               dtde.acceptDrop(DnDConstants.ACTION_COPY);
               @SuppressWarnings("unchecked")
               List<File> files = (List<File>) tr.getTransferData(flavor);
               if (fDirty)
                  switch (JOptionPane.showConfirmDialog(JPad.this,
                                                        SAVE_CHNGS,
                                                        TITLE_AYS,
                                                        JOptionPane.YES_NO_OPTION))
                  {
                     case JOptionPane.YES_OPTION: if (doSave())
                                                     doOpen(files.get(0));
                                                  break;
                     case JOptionPane.NO_OPTION : doOpen(files.get(0));
                  }
               else
                  doOpen(files.get(0));
               dtde.dropComplete(true);
               um.discardAllEdits();
               return;
            }
         dtde.rejectDrop();
      }
      catch (Exception e)
      {
         JOptionPane.showMessageDialog(JPad.this,
                                       "Drop error: "+e.getMessage(),
                                       "Alert",
                                       JOptionPane.ERROR_MESSAGE);
      }
   }
};
dt = new DropTarget(ta, dta);

JPad‘s constructor first subclasses java.awt.dnd.DropTargetAdapter, overriding its void dragOver(DropTargetDragEvent dtde) and void drop(DropTargetDropEvent dtde) methods in order to respond to DropTarget drag and drop events.

A DropTarget drag event arises when an item is dragged over a drop target (an entity associated with a component that is the destination of a drag-and-drop operation). JPad overrides dragOver() to ensure that the mouse cursor always indicates a copy operation.

Copy versus move

Files that are dragged to a text editor are always copied and never moved — it just doesn’t make sense to erase the original file. If the user initiates a drag as a move operation, JPad ensures that the user receives visual feedback that it’s interpreting the drag as a copy operation, so that the user knows the file will not be erased.

A DropTarget drop event arises when the item is released over the drop target. JPad overrides drop() to obtain the first of the list of files being dropped and open it in the text area, after prompting the user to save changes (when necessary).

Finally, JPad instantiates java.awt.dnd.DropTarget, which defines a drop target that connects the text-area component with the drop-target adapter instance. The text area is now ready to accept dropped files.

Implementing the essential: Undo

Most text editors offer an undo feature, which enables users to revert recent edits. JPad supports undo by instantiating the javax.swing.undo package’s UndoManager class and registering an undoable-edit listener with the text area, as shown in Listing 15.

Listing 15. The undoable-edit listener passes undoable edits to the undo manager

um = new UndoManager();
UndoableEditListener uel;
uel = new UndoableEditListener()
{
   @Override
   public void undoableEditHappened(UndoableEditEvent uee)
   {
      um.addEdit(uee.getEdit());
   }
};
ta.getDocument().addUndoableEditListener(uel);

When the text area’s document is modified, the registered javax.swing.event.UndoableEditListener‘s undoableEditHappened() method is invoked with a javax.swing.event.UndoableEditEvent instance, which describes the edit. This instance’s UndoableEdit getEdit() method is invoked to return the edit as a javax.swing.undo.UndoableEdit instance, which is then added to the UndoManager by invoking its boolean addEdit(UndoableEdit anEdit) method.

The action listener registered with the Edit menu’s Undo menu item executes um.undo(); when selected. Because UndoManager‘s void undo() method throws javax.swing.undo.CannotUndoException when there are no edits to be undone, this menu item must only be enabled when there are edits. I accomplish this restriction by executing miUndo.setEnabled(um.canUndo()); in the menuSelected() method of the menu listener that is registered with the Edit menu.

Finally, JPad executes um.discardAllEdits(); to tell the undo manager to discard all undoable edits in response to certain events: creating a new empty document, opening an existing document file, saving the current document to the current document’s file or a new file, or dropping a dragged file onto the text area. The ability to undo old edits wouldn’t make sense in any of these new contexts.

Conclusion to Part 1

In the second part of this article we’ll map the basic UI architecture (content pane, menu system, and event handling) of our Swing notepad application to a JavaFX equivalent: JPadFX.

Jeff Friesen is a freelance tutor and software developer with an emphasis on Java and Android. In addition to writing Java and Android books for Apress, Jeff has written numerous articles on Java and other technologies for JavaWorld, informIT, Java.net, and DevSource. Jeff can be contacted via his website at TutorTutor.ca.