CONCURRENCY IN SWING TEXT
By Timothy Prinzing
Concurrency in GUIs has often been a subject of controversy.
JFCTM/Swing, while not entirely
thread-safe, does offer some support for
concurrency in key areas. This support simplifies the
programming of simple tasks, while leaving the door open for the
creation of more complex applications.
Text tends to come in large quantities that often can't be
satisfactorily dealt with using brute force while keeping the GUI
responsive. Making use of concurrency for text-oriented interfaces
is natural because most the work needed to make things current
have negligible visibility. That is why the JFC/Swing text package
offers some support for concurrency.
This article examines the support for concurrency provided by
the Swing text package, and then shows how to perform text layout
asynchronously to the event dispatching thread in an interactive
display.
This article contains five major sections:
The examples in the article require a recent version of JFC/Swing,
as a number of concurrency related bugs have been fixed in newer
versions. To run the examples, you should use JFC/Swing 1.1.1 beta
1 or later, or JDK1.2.2. Links to the source files are provided
in the sections that describe them. To download a set of a set of
prebuilt class files stored as a zipped.jar file, follow
this link. (To use the .jar file,
you'll have to unzip it using PKZip, WinZip, or some other unzipping
utility.)
Asynchronous Model Changes
The primary form of concurrency support in the text package is
designed to support building and maintenance (also called mutations)
of the document model on a separate thread from the event dispatching
thread. A concurrent application will typically interact with the
model and have no direct interest in the GUI. This allows the control
and view functionality built into the Swing text package to maintain
the simplicity of operating on the event dispatching thread. What
makes it particularly useful is that parsing from an archive is
generally a very time-consuming process that actually amounts to
making a series of edits to the model.
One of our goals in designing the Swing text package was to support
developers who want to create a customized view but have no interest
in dealing with concurrency. To accomplish that goal, the TextUI
implementation serializes access to the view. At any given time,
a thread may either be actively mutating the model or performing
some operation that doesn't mutate the document model, such as rendering
or performing layout-related operations. Because the model is the
source of the mutations (and the center of the MVC relationship),
it is a convenient place to perform locking. Therefore, the model
provides a locking mechanism that supports either one writer or
multiple readers. The text package makes use of the locking mechanism
to serialize access to the view(s) of the model.
The model change notification updates the view with a write lock.
The purpose of notifying with the write lock held is to guarantee
that threads with read-only interest in the model are not using
the view while the view is being updated with the changes that have
been made to the model. For example, the event dispatching thread
renders the view with a read-lock and is guaranteed the model is
not changing while the event dispatching thread is performing its
operations. Serializing access to the UI in this way makes writing
the view implementations substantially easier than trying to support
concurrency through atomic operations on each of the objects that
make up the UI. It also reduces the cost for developers who choose
not to make use of concurrency; that is, the approach is not fine-grained
and represents minimal overhead over an unthreaded approach.
The preceding diagram illustrates the serialization of access to
the UI between the event dispatching thread and another thread that
is actively mutating the model. The javax.swing.plaf.basic.BasicTextUI
and javax.swing.text.AbstractDocument classes implement
this relationship. The rendering and layout occur in an implementation
of the javax.swing.text.View class, which can be written
with minimal concern for thread safety. The considerations for View
implementers are as follows:
- There will likely be multiple MVC relationships, so it is unlikely
that a View will be able to use any static variables unless they
are thread-safe.
- For methods where the flow of control is downward -- that is,
methods called by the TextUI implementation on a View or called
by a parent View to a child View -- there should only be one thread
at a time inside a View instance that is traversing downward in
the tree. These methods can be implemented with complete disregard
to locking unless the class specifically wants to add additional
concurrency, such as the AsyncBoxView presented here.
- For methods where the flow of control is upward -- such as methods
that a child View calls on its parent, and [methods that] the
root View calls on the associated TextUI -- the call should be
capable of occurring asynchronously.
With the exception of the JEditorPane.setPage method,
there are no asynchronous mutations unless the developer specifically
arranges for them by creating a thread to mutate the document model.
This feature of the text package leaves it up to the developer to
determine whether concurrency is used. Loading a file is treated
as a series of mutations to the model. If this operation is carried
out in response to a user action -- for example, in response to
the pressing of a button in the GUI -- then that thread (the event
dispatching thread) will be used to load and the operation will
be performed synchronously. Alternatively, the developer might create
a thread and perform the loading on that thread as a series of asynchronous
mutations to the model.
A simple class to load a document might look like this:
class AsyncLoader extends Thread {
AsyncLoader(Document doc, Reader in) {
super("document-loader");
setPriority(Thread.MIN_PRIORITY);
this.in = in;
this.doc = doc;
}
public void run() {
try {
char[] buff = new char[4096];
int nch;
while ((nch = in.read(buff, 0, buff.length)) != -1) {
doc.insertString(doc.getLength(), new String(buff, 0, nch), null);
}
} catch (Throwable e) {
e.printStackTrace();
System.exit(1);
}
}
private Reader in;
private Document doc;
}
The preceding example functions with plain text, but for more interesting
kinds of formats such as HTML a fairly large amount of time is spent
parsing and determining how to modify the model. To leave the Document
free of a write-lock for as much time as possible, the HTMLDocument
builds up a set of tokens that describe the tree edit that is desired
while the input stream is parsed, so that the actual amount of time
that is spent with a write-lock acquired is small. After the changes
have been made, the document remains locked, while notifying the
listeners. This is because the Java Beans event model requires all
listeners to be notified of the new state prior to publishing the
effects of further mutations. The class that provides most of this
functionality is DefaultStyledDocument.ElementBuffer ,
which is accessed via the DefaultStyledDocument.insert
method. The ElementBuffer functionality is described
in
another article
The preceding diagram shows an example of how two threads might
interact. It's what one potentially gets when using the HTMLEditorKit
with JEditorPane.setPage . The green portions of the
timing bars indicate the model is in an unlocked state. Red portions
indicate a lock is held. Yellow indicates the thread is stopped
waiting to acquire the lock. The red portions of the timing for
the model thread indicate a write lock is held. The red portions
of the timing bar for the view thread indicate a read lock is held.
The smaller the red and yellow areas on the event dispatching thread
the better the GUI response.
The JEditorPane.setPage method spawns a thread to
load the document if the document is of type AbstractDocument and
has a value greater than or equal to zero specified for the asynchronousLoadPriority
property. If the document advertises a thread priority to load at
a thread will be created and run to load the document.
Each EditorKit implementation can control whether or not URLs
of its type are loaded asynchronously by using the setting of the
asynchronousLoadPriority property. There is an additional
property (called tokenThreshold ) on HTMLDocument that
controls how many tokens (that describe the desired edit) are buffered
before attempting to mutate the document model. The larger the setting,
the wider the red/yellow bars (lock and wait times) will get. The
smaller the setting, the more frequent are the updates to the view(s),
resulting in a higher level of repeated layout and rendering operations
triggered by the updates.
Asynchronous Layout
For interactive views, layout and rendering can be an iterative
process. The thread-safe methods Component.repaint
and JComponent.revalidate are for scheduling future
paints and layout. With this capability we can accomplish a number
of things:
- The responsiveness of the UI can be substantially improved
because the event dispatching thread doesn't have to wait for
layout to complete. Performing layout is inherently computationally
expensive and much of it may not be visible so performing calculations
not needed to display can be done as a low priority task.
- Updates from the model will be substantially more efficient
because the updates don't have to wait for layout to complete,
and the updates from mutating the model will likely mean changes
to the layout anyway.
- If the layout is performed asynchronously, it is much easier
to provide incremental updates to the layout. For some kinds of
layout that involve positioning many objects, this can result
in substantial timesaving.
Views are produced by a ViewFactory implementation,
which gives the developer substantial control over how the view
represents the model. Replacing the ViewFactory is
a good strategy for increasing the level of concurrency for those
cases that will benefit from it. A text field has so little work
involved in layout that there is no benefit to computing the layout
asynchronously. An HTML view does so much work that trying to do
it on the event dispatching thread will cause the user interface
to freeze. Printing could use a ViewFactory implementation
that is different from the factory used by the GUI. It can share
most of the building blocks used for interactive views, but strategically
replace certain parts. For example, a printing ViewFactory
might use a View that does page breaking to represent
an element, where the View implementation used for the GUI might
be something like the AsyncBoxView example that can defer work to
keep the GUI responsive.
The javax.swing.text.View implementation provided
in this article is an example of performing layout asynchronous
to the event dispatching thread. A future version of swing (version
1.3) will have this functionality built in.
For print views, layout and rendering can all happen on another
thread that is completely unrelated to the event dispatching thread.
The locking strategy would depend upon whether or not mutations
would be allowed while the model is being printed. If a read lock
is acquired for the entire duration of the layout and render process,
the user would be prevented from modifying the model but they could
view it. A better implementation would watch for mutations while
preparing the print job and do fixes to the print view if it hadn’t
yet been printed. We don't currently have any direct support of
printing. A future article will provide some examples and a future
version of Swing will have direct support for printing.
Asynchronous Layout Example
One View implementation that performs layout asynchronously is
AsyncBoxView. Many text views use the box layout pattern, so an
implementation that performs layout asynchronously is a useful one.
The javax.swing.text.BoxView class provides a general-purpose
box layout that can be easily customized. However, when a large
box needs to be laid out, performance could be improved by avoiding
the time-consuming task of considering all the children when doing
layout. That is the direction that was taken in the design of the
AsyncBoxView class. The class was designed with these goals in mind:
- Keep the GUI thread active by doing as little work as possible
on it.
- Minimize the number of threads used for doing layout.
- Update visible areas before updating those that are not visible.
- Service model/view translations as quickly as possible and synchronously.
- Minimize the time required to react to changes that are broadcast
from the model (these changes may not be visible).
Design Overview
The most time-consuming part of laying out a large box
is calculating preferred sizes for the children, and then waiting
for the actual layout that the children perform. If these two
operations are performed synchronously, it may even turn out that
a lot of this work is useless. For instance, the view may have
had its size changed again, or an update from the model may have
changed the overall size requirements that result in a resize.
Further, access to the view hierarchy is serialized with respect
to mutations. This means a read lock should not be held any longer
than necessary, so that updates from the model can be squeezed
in. Since changes are natural, it is important to deal with them
in the least time-consuming way possible. To accomplish this,
AsyncBoxView does the following things:
- The management of the size of the child is separated from the
management of where the child view is located. This separates
the time-consuming functionality of a layout from the display-oriented
functionality of placement so that the time consuming part can
be performed on a separate thread. The size of each child is also
managed separately -- that is the children are not aligned with
respect to each other.
- The call to a method on each child is granularity of the read
lock on the document for layout operations. This is substantially
simpler than alternative kinds of controls of the read-lock lifetime;
in general, it gives the mutating thread pretty good access to
the model. Choosing a good strategy from the
ViewFactory
implementation will ensure that this is true.
- The locations of the children are calculated only if they're
actually needed. The overall length along the major axis is maintained
separately, so that the individual locations are not needed to
communicate with the parent view. This makes it easier to calculate
the size incrementally and defers some work that might be invalidated
prior to becoming useful.
- The event dispatching thread is used to perform the layout if
the child is actually visible. This makes the display responsive,
no matter what the state of the layout queue might be. The order
in which tasks sit in the queue becomes inconsequential when the
only work being done is not visible.
- There can be multiple layout queues, but by default there will
be one queue to service all of the instances of
AsyncBoxView .
The default queue is serviced by a single dedicated thread running
at a low priority.
- Layout work that has been queued may be invalidated at some
time prior to to the time that it is actually executed. This might
be because the size has changed again, or because the child has
become visible. Trying to clean the queue can become expensive,
so the expense is avoided by making all the layout tasks able
to discover if their services are no longer needed, or by immediately
adapting to a new set of constraints. This mechanism allows the
queue to automatically empty quickly for completed work, and is
possible because order in the queue is inconsequential.
- While not specifically addressing a goal, it would be useful
if the example could be customized to the floating-point based
coordinates used in Java 2D.
The example itself needs to function with JDK 1.1 as well,
however. To make it possible to create an entirely floating-point
based solution, the AsyncBoxView class uses floating point everywhere
possible, so an entirely floating-point solution is possible
with only minor changes from a subclass -- that is, the only
integers used are the incoming allocation and outgoing child
allocation.
In order to calculate the layout incrementally, the AsyncBoxView
advertises its preferences as being flexible along the minor axis
-- that is, the axis orthogonal to the axis being tiled -- and rigid
along the major axis along which the children are being tiled. The
size along the major axis is the sum of the children's preferred
span, which can be determined incrementally. This incremental calculation
gives the view the ability to publish its size to the parent relatively
cheaply, and as frequently as it wants. The actual location of the
children is not calculated unless they become visible or are needed
for caculating a model-to-view coordinate translation. The following
diagram shows the preferences for a vertical box (i.e. with a major
axis of View.Y_AXIS ).
Because many views can make use of a border area around
the children, the AsyncBoxView supports a set of floating
point insets around the area given to the children. This inset area
might be used to draw borders, create margins, and so on. The following
diagram shows the insets:
More Details about the AsyncBoxView Class
The following paragraphs provide a few more important details about
the AsyncBoxView class, its nested classes, and the layout queue.
The classes
The AsyncBoxView class is primarily a facade that delegates
to other objects to accomplish its tasks. The actual layout is
performed by nested classes, and the layout thread is managed
as a separate class to enable multiple views to use the class's
services at the same time.
- ChildState
- This nested class is responsible for representing the layout
state of a child view. The layout thread (or the event dispatching
thread, depending upon whether the child view is needed to be
current) may update it. This is the expensive part the layout,
and represents the child size portion of the layout. This is
implemented to run as a task (i.e. it does its work as an implementation
of the Runnable interface) so that is can be placed on the LayoutQueue
without allocating additional memory. There is one of these
for each child referenced.
- ChildLocator
- This nested class is responsible for representing the location
of the children. This is used to determine child view allocations
so that
AsyncBoxView can paint the children and
perform model/view translation via the children. There is exactly
one of these per instance of AsyncBoxView .
- LayoutQueue
- This is a very simple layout queue that takes objects implementing
the Runnable interface and executes them sequentially. By default,
there is one LayoutQueue that is shared by all instances of
AsyncBoxView .
The view implementation is composed of the following classes, packaged
in a package called pre13 :
How It Works
The AsyncBoxView extends the basic support
provided by the document lock by adding another thread to do layout.
The layout thread always grabs a read lock on the document to
ensure that the document will not be changing while layout operations
are being performed. The lock is held for the length of time needed
to make one method call on a child of the AsyncBoxView. This limited
lock aquisition gives a mutating thread a chance to change the
document and notify the views, but protects against any unwanted
interaction with a changing model. Unfortunately, aquiring a read
lock does nothing to protect the layout thread from unwanted interaction
with the event dispatching thread, which can acquire a read lock
at the same time. The methods that propagate downward to the children
all expect that only one thread will be active in a given View
instance at a time unless the view was specifically written to
support concurrency (as AsyncBoxView was). Each child
view has an associated instance of the ChildState class that represents
what is known about the child. Each ChildState record is implemented
as a Runnable to enable it to be placed upon the LayoutQueue which
will execute it on a layout thread. It can also be run directly
by the event dispatching thread. The ChildState object is used
to synchronize access to the child (a view subtree). This synchronization
keeps the layout thread and event dispatching thread from both
traveling down the same subtree at the same time. An example of
a layout thread and event dispatching thread operating in different
parts of the tree is illustrated by the diagram below. The root
of the tree is an instance of AsyncBoxView and the
event dispatching thread is actively painting a child that is
currently visible while another child is being processed by a
layout thread.
The code in ChildState to update the child state is
shown below. In this example, potentially expensive method calls
are highlighted in red. The preferences along the minor axis (due
to being flexible) are determined by calling the getMinimumSpan ,
getPreferredSpan , and getMaximumSpan
methods. These values only need to be fetched once unless the
child changes what it wants to publish along this axis (which
is common). If the child changes its preference, it calls preferenceChanged ,
which propagates upward through the tree. Along the major axis,
the child is given exactly what it prefers. The View can publish
this upward through the tree when it wants; this is an interesting
customization area for an asynchronous View. What is published
is the required span to tile the preferred span of the children.
This is a rigid value (i.e. the mininum value and maximum value
are equal to the preferred value), so a properly functioning parent
will give the desired allocation to fulfill the tile operation.
Finally, the child's size is set, triggering the layout in the
child (which may or may not be synchronous).
void updateChild() {
boolean minorUpdated = false;
synchronized(this) {
if (! minorValid) {
int minorAxis = getMinorAxis();
min = child.getMinimumSpan(minorAxis);
pref = child.getPreferredSpan(minorAxis);
max = child.getMaximumSpan(minorAxis);
minorValid = true;
minorUpdated = true;
}
}
if (minorUpdated) {
minorRequirementChange(this);
}
boolean majorUpdated = false;
float delta = 0.0f;
synchronized(this) {
if (! majorValid) {
float old = span;
span = child.getPreferredSpan(axis);
delta = span - old;
majorValid = true;
majorUpdated = true;
}
}
if (majorUpdated) {
majorRequirementChange(this, delta);
locator.childChanged(this);
}
synchronized(this) {
if (! childSizeValid) {
float w;
float h;
if (axis == X_AXIS) {
w = span;
h = getMinorSpan();
} else {
w = getMinorSpan();
h = span;
}
childSizeValid = true;
child.setSize(w, h);
}
}
}
The methods that travel upward through the parent should
be written in a thread-safe way. Most of these methods simply
fetch resources from the hosting component, and aren't a concern
to view implementers, as the default implementation is generally
called. An exception to this is the preferenceChanged
method, which typically changes some state in a composite view
because it needs to undergo layout again. AsyncBoxView
is a composite view with children, so it needs to consider the
ramifications of a call on preferenceChanged. The AsyncBoxView
is different from a synchronous view in the implementation will
not immediately propagate the call upward if it's preferences
were changed because it has the ability to propagate the change
on the layout thread. If a child changes its requirements (by
calling preferenceChanged on the AsyncBoxView ),
its associated ChildState record is marked and the
ChildState is placed on the layout queue, along with
a Runnable to publish the new layout to the parent
of the AsyncBoxView . The code to accomplish this
[operation] is:
public synchronized void preferenceChanged
(View child, boolean width, boolean height) {
if (child == null) {
getParent().preferenceChanged(this, width, height);
} else {
if (changing != null) {
View cv = changing.getChildView();
if (cv == child) {
// size was being changed on the child, no need to
// queue work for it.
changing.preferenceChanged(width, height);
return;
}
}
int index = getViewIndexAtPosition(child.getStartOffset(),
Position.Bias.Forward);
ChildState cs = getChildState(index);
cs.preferenceChanged(width, height);
LayoutQueue q = getLayoutQueue();
q.addTask(cs);
q.addTask(flushTask);
}
}
When child views are added from the AsyncBoxView, AsyncBoxView
creates a ChildState instance to represent the View and adds it
to the layout queue for processing. After all the new state records
have been added, a Runnable to publish the resulting changes to
the parent of the AsyncBoxView is also placed on the queue. This
happens as a result of adding the AsyncBoxView to a View hierarchy
with the setParent method, or as a result of notification from
the model via the insertUpdate , removeUpdate ,
and changedUpdate methods. The notification methods
will, of course, be called with a write lock held on the document.
The newly created representatives of the child state will start
being processed to keep the layout of AsyncBoxView current.
public void replace(int offset, int length, View[] views) {
synchronized(stats) {
LayoutQueue q = getLayoutQueue();
ChildState[] s = new ChildState[views.length];
for (int i = 0; i < s.length; i++) {
s[i] = createChildState(views[i]);
q.addTask(s[i]);
}
stats.replace(offset, length, s);
q.addTask(flushTask);
}
}
After a change in the view hierarchy, typically a repaint
will have been triggered for some region of the display. If a
part of the AsyncBoxView is visible, it will get a paint request
that requires that the children intersecting the visible region
be current prior to traversing through them. The event dispatching
thread will request the locations from the ChildLocator, which
determines how far down the major axis the first child is that
intersects the visible region. The ChildLocator will ensure that
each intersected child's state is made current (by running it
on the event dispatching thread) and then will propagate the paint
method (while synchronized on the ChildState of the child being
painted).
public void paint(Graphics g, Shape alloc) {
synchronized (locator) {
locator.setAllocation(alloc);
locator.paintChildren(g);
}
}
A modelToView or viewToModel method call work like paint,
except the propagation is to a single child rather than multiple
children.
public Shape modelToView(int pos, Shape a, Position.Bias b)
throws BadLocationException {
int index = getViewIndexAtPosition(pos, b);
Shape ca = locator.getChildAllocation(index, a);
// forward to the child view, and make sure we don't
// interact with the layout thread by synchronizing
// on the child state.
ChildState cs = getChildState(index);
synchronized (cs) {
View cv = cs.getChildView();
Shape v = cv.modelToView(pos, ca, b);
return v;
}
}
public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) {
int pos; // return position
int index; // child index to forward to
Shape ca; // child allocation
// locate the child view and it's allocation so that
// we can forward to it. Make sure the layout thread
// doesn't change anything by trying to flush changes
// to the parent while the GUI thread is trying to
// find the child and it's allocation.
synchronized (locator) {
index = locator.getViewIndexAtPoint(x, y, a);
ca = locator.getChildAllocation(index, a);
}
// forward to the child view, and make sure we don't
// interact with the layout thread by synchronizing
// on the child state.
ChildState cs = getChildState(index);
synchronized (cs) {
View v = cs.getChildView();
pos = v.viewToModel(x, y, ca, biasReturn);
}
return pos;
}
If there is a change in the span along the minor axis
(via a call from the parent to the setSize method), all the ChildState
records are invalidated and placed upon the layout queue, and
a task to publish the results to the parent is also queued. Any
work previously on the layout queue for this view will immediately
begin performing layout of the children according to the new span
setting along the minor axis. When the redundant task is encountered
again, it will simply notice there is no work to do.
public void setSize(float width, float height) {
float targetSpan;
if (axis == X_AXIS) {
targetSpan = height - getTopInset() - getBottomInset();
} else {
targetSpan = width - getLeftInset() - getRightInset();
}
if (targetSpan != minorSpan) {
minorSpan = targetSpan;
// mark all of the ChildState instances as needing to
// resize the child, and queue up work to fix them.
int n = getViewCount();
LayoutQueue q = getLayoutQueue();
for (int i = 0; i < n; i++) {
ChildState cs = getChildState(i);
cs.childSizeValid = false;
q.addTask(cs);
}
q.addTask(flushTask);
}
}
Areas for Improvement
The example makes no attempt to be clever about when
it chooses to publish its changes upward to the parent view. In
many cases this doesn't matter very much, but if multiple AsyncBoxView
instances are nested (such as in nested html tables), the delay
of propagating the layout changes upward becomes visible. Intermediate
results can be published easily, and an improved implementation
would take advantage of that.
There are a number of performance enhancements that
could be made for calculating the requirements along the minor
axis. These calculations are performed on the layout thread, via
a brute-force visitation of all the children.
The ChildLocator can often locate the correct child
for viewToModel() calculations via a binary search.
This would fall out rather naturally if the 1.2 collections were
used.
An Example usage with StyledEditorKit
A relatively simple usage of the asynchronous box is to alter the
StyledEditorKit ViewFactory implementation to produce an AsyncBoxView
for the vertical arrangement of the paragraphs, rather than using
BoxView. For larger documents this change makes a substantial difference
in the responsiveness of the UI. The classes are packaged in the
examples.async package. The class examples.async.StyledEditorKit
provides a replacement (extension) for javax.swing.text.StyledEditorKit
that can be used as an alternative in JTextPane. The class examples.async.Example1
illustrates the use of the replacement EditorKit implementation.
The element structure used by DefaultStyledDocument is discussed
in another
article. From that model we can easily build a view that is
a tree of objects extending the View class. By default, this is
a set of objects based upon BoxView, ParagraphView, LabelView, ComponentView,
and IconView. These are all implementations of the View class.
The AsyncStyledEditorKit class implements the ViewFactory
interface. The implementation of the ViewFactory.create
method creates an AsyncBoxView if the element is the root element
of the DefaultStyledDocument; otherwise the default factory of the
superclass is used to produce the views. The following figure illustrates
the spatial relationship of the views in the view hierarchy.
public View create(Element elem) {
String kind = elem.getName();
if ((kind != null) &&
(kind.equals(AbstractDocument.SectionElementName))) {
return new AsyncBoxView(elem, View.Y_AXIS);
}
ViewFactory f = super.getViewFactory();
return f.create(elem);
}
The StyleExample.java file makes
up the example. The example can be executed as an application by
executing a command something like:
java StyleExample filename
Where filename is the name
of a file you would like to load.
An Example of Usage with HTMLEditorKit
The factory for the asynchronous HTMLEditorKit works much like
the asynchronous StyledEditorKit, except that it can't produce AsyncBoxView
directly because more functionality is needed to translate the CSS
attributes recognized. The nested BlockView class operates like
the javax.swing.text.html.BlockView class, except that
it extends AsyncBoxView instead of BoxView. This is used to represent
the body element, which typically has a large number of children.
Many pages are also composed entirely of tables, so using the AsyncBoxView
derivative for table cells will cause their contents to be handled
asynchronously. One could be more clever about when asynchronous
views are used, but the approach shown is simple.
The HtmlExamle.java file makes up
the example. The example can be executed as an application by executing
a command something like:
java HtmlExample url
Where url is the name of an
URL you would like to load.
|