|
Fisheye Calendar This tutorial will
illustrate how you
might build a tabular fisheye interface using Piccolo.
Clicking on one cell will give that cell the fisheye focus
while shrinking the surrounding cells. Such an
interface might be useful when you don't have much screen
real estate to deal with. |
|
 Download the complete code sample in
Java or
C#.
Play with the interface.
Overall Architecture
In this example, we will create two custom nodes, a DayNode and
a CalendarNode. The DayNode will be responsible
for rendering the contents of a single cell within the calendar.
This node will use semantic zooming to control how a day is
rendered, depending on whether or not that day is focused.The
CalendarNode will be responsible for laying out all of the DayNodes
in a tabular fashion. Every DayNode will be added as a child
to the CalendarNode. This node will also handle the user
interaction, focusing on a cell in response to a click, and
animating transitions when requested to do so.
We will then create a reusable TabularFisheye component that extends PCanvas.
This component will add the calendar node to the scene-graph and resize it when
the component is resized. Finally, we will create a wrapper
window called TabularFisheyeTester and add our new component to the window.
1.
Create a Day Node
We will create a node that will be responsible for
rendering the contents of a single cell within in the
calendar. This node will use semantic zooming to
control how each day is rendered depending on whether or not
the day has the fisheye focus. The day that is
expanded both vertically and horizontally has the focus.
Here, we will create the day node. Add the following
class to your project.
Java |
C#
static class DayNode extends PNode {
boolean hasWidthFocus;
boolean hasHeightFocus;
ArrayList lines;
int week;
int day;
String dayOfMonthString;
public DayNode(int week, int day) {
lines = new ArrayList();
lines.add("7:00 AM Walk the dog.");
lines.add("9:30 AM Meet John for Breakfast.");
lines.add("12:00 PM Lunch with Peter.");
lines.add("3:00 PM Research Demo.");
lines.add("6:00 PM Pickup Sarah from gymnastics.");
lines.add("7:00 PM Pickup Tommy from karate.");
this.week = week;
this.day = day;
this.dayOfMonthString = Integer.toString((week * 7) + day);
setPaint(Color.BLACK);
}
public int getWeek() {
return week;
}
public int getDay() {
return day;
}
public boolean hasHeightFocus() {
return hasHeightFocus;
}
public void setHasHeightFocus(boolean hasHeightFocus) {
this.hasHeightFocus = hasHeightFocus;
}
public boolean hasWidthFocus() {
return hasWidthFocus;
}
public void setHasWidthFocus(boolean hasWidthFocus) {
this.hasWidthFocus = hasWidthFocus;
}
protected void paint(PPaintContext paintContext) {
Graphics2D g2 = paintContext.getGraphics();
g2.setPaint(getPaint());
g2.draw(getBoundsReference());
g2.setFont(CalendarNode.DEFAULT_FONT);
float y = (float) getY() + CalendarNode.TEXT_Y_OFFSET;
paintContext.getGraphics().drawString(dayOfMonthString,
(float) getX() + CalendarNode.TEXT_X_OFFSET, y);
if (hasWidthFocus && hasHeightFocus) {
paintContext.pushClip(getBoundsReference());
for (int i = 0; i < lines.size(); i++) {
y += 10;
g2.drawString((String)lines.get(i),
(float) getX() + CalendarNode.TEXT_X_OFFSET, y);
}
paintContext.popClip(getBoundsReference());
}
}
}
Web Accessibility
class DayNode : PNode {
bool hasWidthFocus;
bool hasHeightFocus;
ArrayList lines;
int week;
int day;
String dayOfMonthString;
public DayNode(int week, int day) {
lines = new ArrayList();
lines.Add("7:00 AM Walk the dog.");
lines.Add("9:30 AM Meet John for Breakfast.");
lines.Add("12:00 PM Lunch with Peter.");
lines.Add("3:00 PM Research Demo.");
lines.Add("6:00 PM Pickup Sarah from gymnastics.");
lines.Add("7:00 PM Pickup Tommy from karate.");
this.week = week;
this.day = day;
this.dayOfMonthString = ((week * 7) + day) + "";
Brush = Brushes.Black;
}
public int Week {
get { return week; }
}
public int Day {
get { return day; }
}
public bool HasHeightFocus {
get {
return hasHeightFocus;
}
set {
hasHeightFocus = value;
}
}
public bool HasWidthFocus {
get {
return hasWidthFocus;
}
set {
hasWidthFocus = value;
}
}
protected override void Paint(PPaintContext paintContext) {
Graphics g = paintContext.Graphics;
g.DrawRectangle(Pens.Black, Bounds.X, Bounds.Y,
Bounds.Width, Bounds.Height);
float y = (float) Y + CalendarNode.TEXT_Y_OFFSET;
g.DrawString(dayOfMonthString, CalendarNode.DEFAULT_FONT,
Brush, (float) X + CalendarNode.TEXT_X_OFFSET, y);
if (hasWidthFocus && hasHeightFocus) {
paintContext.PushClip(new Region(Bounds));
for (int i = 0; i < lines.Count; i++) {
y += 10;
g.DrawString((String)lines[i], CalendarNode.DEFAULT_FONT,
Brush, X + CalendarNode.TEXT_X_OFFSET, y);
}
paintContext.PopClip();
}
}
}
Web Accessibility
This node stores two boolean values that indicate whether it
should be expanded vertically and horizontally, a list of appointments, two integer values for
the week and the day that the cell lies on, and a string
representation of the day of the month.
The paint method draws the day of the month in the upper left
corner. And, when the cell has the focus, it also renders
the list of appointments, since there will be more space for
this information. A cell has the focus when it is expanded
both vertically and horizontally. See the picture above.
2.
Create a Calendar Node
We will create a node that will be responsible for laying
out all of the day nodes within the tabular calendar.
- Here, we will create the calendar node. Add the following
class to your project.
Java |
C#
static class CalendarNode extends PNode {
static int DEFAULT_NUM_DAYS = 7;
static int DEFAULT_NUM_WEEKS = 12;
static int TEXT_X_OFFSET = 1;
static int TEXT_Y_OFFSET = 10;
static int DEFAULT_ANIMATION_MILLIS = 250;
static float FOCUS_SIZE_PERCENT = 0.65f;
static Font DEFAULT_FONT = new Font("Arial", Font.PLAIN, 10);
int numDays = DEFAULT_NUM_DAYS;
int numWeeks = DEFAULT_NUM_WEEKS;
int daysExpanded = 0;
int weeksExpanded = 0;
public CalendarNode() {
for (int week = 0; week < numWeeks; week++) {
for (int day = 0; day < numDays; day++) {
addChild(new DayNode(week, day));
}
}
}
public DayNode getDay(int week, int day) {
return (DayNode) getChild((week * numDays) + day);
}
protected void layoutChildren(boolean animate) {
double focusWidth = 0;
double focusHeight = 0;
if (daysExpanded != 0 && weeksExpanded != 0) {
focusWidth = (getWidth() * FOCUS_SIZE_PERCENT) / daysExpanded;
focusHeight = (getHeight() * FOCUS_SIZE_PERCENT) / weeksExpanded;
}
double collapsedWidth = (getWidth() - (focusWidth * daysExpanded))
/ (numDays - daysExpanded);
double collapsedHeight = (getHeight() - (focusHeight * weeksExpanded))
/ (numWeeks - weeksExpanded);
double xOffset = 0;
double yOffset = 0;
double rowHeight = 0;
DayNode each = null;
for (int week = 0; week < numWeeks; week++) {
for (int day = 0; day < numDays; day++) {
each = getDay(week, day);
double width = collapsedWidth;
double height = collapsedHeight;
if (each.hasWidthFocus()) width = focusWidth;
if (each.hasHeightFocus()) height = focusHeight;
if (animate) {
each.animateToBounds(xOffset, yOffset, width,
height, DEFAULT_ANIMATION_MILLIS).setStepRate(0);
} else {
each.setBounds(xOffset, yOffset, width, height);
}
xOffset += width;
rowHeight = height;
}
xOffset = 0;
yOffset += rowHeight;
}
}
}
Web Accessibility
class CalendarNode : PNode {
public static int DEFAULT_NUM_DAYS = 7;
public static int DEFAULT_NUM_WEEKS = 12;
public static int TEXT_X_OFFSET = 1;
public static int TEXT_Y_OFFSET = 1;
public static int DEFAULT_ANIMATION_MILLIS = 250;
public static float FOCUS_SIZE_PERCENT = 0.65f;
public static Font DEFAULT_FONT = new Font("Arial", 7);
int numDays = DEFAULT_NUM_DAYS;
int numWeeks = DEFAULT_NUM_WEEKS;
int daysExpanded = 0;
int weeksExpanded = 0;
public CalendarNode() {
for (int week = 0; week < numWeeks; week++) {
for (int day = 0; day < numDays; day++) {
AddChild(new DayNode(week, day));
}
}
}
public DayNode GetDay(int week, int day) {
return (DayNode) GetChild((week * numDays) + day);
}
public void LayoutChildren(bool animate) {
float focusWidth = 0;
float focusHeight = 0;
if (daysExpanded != 0 && weeksExpanded != 0) {
focusWidth = (Width * FOCUS_SIZE_PERCENT) / daysExpanded;
focusHeight = (Height * FOCUS_SIZE_PERCENT) / weeksExpanded;
}
float collapsedWidth = (Width - (focusWidth * daysExpanded))
/ (numDays - daysExpanded);
float collapsedHeight = (Height - (focusHeight * weeksExpanded))
/ (numWeeks - weeksExpanded);
float xOffset = 0;
float yOffset = 0;
float rowHeight = 0;
DayNode each = null;
for (int week = 0; week < numWeeks; week++) {
for (int day = 0; day < numDays; day++) {
each = GetDay(week, day);
float width = collapsedWidth;
float height = collapsedHeight;
if (each.HasWidthFocus) width = focusWidth;
if (each.HasHeightFocus) height = focusHeight;
if (animate) {
each.AnimateToBounds(xOffset, yOffset, width,
height, DEFAULT_ANIMATION_MILLIS).StepInterval = 0;
} else {
each.SetBounds(xOffset, yOffset, width, height);
}
xOffset += width;
rowHeight = height;
}
xOffset = 0;
yOffset += rowHeight;
}
}
}
Web Accessibility
The constructor adds all of the day nodes as children to the
calendar node. The
actual number of days in the calendar is determined by the numDays
and numWeeks fields. The GetDay
method retrieves a particular day node.
The most interesting part
of the class is the LayoutChildren method. This
is where all of the day nodes are sized and arranged in a tabular
fashion. Days that are expanded vertically receive a certain
percentage of the overall height and days expanded horizontally
receive a certain percentage of the overall width. The remaining
space is divided equally among the unexpanded days. This method
should not be confused with PNode's LayoutChildren method,
which gets called whenever a node's bounds or one of its desendent's
bounds change. We could have overridden that method to do our
layout. But, since we want the option to animate the layout we
create a custom method, which takes an animate flag to
indicate if the children should be animated to their new positions.
When the user clicks on a node, we will
call this method with animate set to
true. And when the calendar node is resized, we will call this
method
with animate set to false.
- Next we will add the interaction. Add this code to your the
CalendarNode defined
above. For the Java version, you should add
the anonymous event listener class to the constructor.
Java |
C#
CalendarNode.this.addInputEventListener(new PBasicInputEventHandler() {
public void mouseReleased(PInputEvent event) {
DayNode pickedDay = (DayNode) event.getPickedNode();
if (pickedDay.hasWidthFocus && pickedDay.hasHeightFocus) {
setFocusDay(null, true);
} else {
setFocusDay(pickedDay, true);
}
}
});
public void setFocusDay(DayNode focusDay, boolean animate) {
for (int i = 0; i < getChildrenCount(); i++) {
DayNode each = (DayNode) getChild(i);
each.hasWidthFocus = false;
each.hasHeightFocus = false;
}
if (focusDay == null) {
daysExpanded = 0;
weeksExpanded = 0;
} else {
focusDay.hasWidthFocus = true;
daysExpanded = 1;
weeksExpanded = 1;
for (int i = 0; i < numDays; i++) {
getDay(focusDay.week, i).hasHeightFocus = true;
}
for (int i = 0; i < numWeeks; i++) {
getDay(i, focusDay.day).hasWidthFocus = true;
}
}
layoutChildren(animate);
}
Web Accessibility
public override void OnMouseUp(PInputEventArgs e) {
DayNode pickedDay = (DayNode) e.PickedNode;
if (pickedDay.HasWidthFocus && pickedDay.HasHeightFocus) {
SetFocusDay(null, true);
} else {
SetFocusDay(pickedDay, true);
}
}
public void SetFocusDay(DayNode focusDay, bool animate) {
for (int i = 0; i < ChildrenCount; i++) {
DayNode each = (DayNode) GetChild(i);
each.HasWidthFocus = false;
each.HasHeightFocus = false;
}
if (focusDay == null) {
daysExpanded = 0;
weeksExpanded = 0;
} else {
focusDay.HasWidthFocus = true;
daysExpanded = 1;
weeksExpanded = 1;
for (int i = 0; i < numDays; i++) {
GetDay(focusDay.Week, i).HasHeightFocus = true;
}
for (int i = 0; i < numWeeks; i++) {
GetDay(i, focusDay.Day).HasWidthFocus = true;
}
}
LayoutChildren(animate);
}
Web Accessibility
When the mouse is released, and the picked day is unfocused, we
will set the focus to that day. If the picked day already has
the focus, we will remove the focus from that day. The SetFocusDay method
will determine whether or not each day in the calendar should be
expanded vertically or horizontally and set that's node's focus properties accordingly.
Then LayoutChildren will be called to animate the nodes
to their new layout.
3.
Create a Fisheye Canvas
Unlike the previous tutorials, we are not going to extend
PForm or PFrame in this example. Instead we will make
a reusable component that extends PCanvas and we will add
that component to our Java or .NET window.
We extend PCanvas and add the calendar
node to the scene-graph.
Java |
C#
public class TabularFisheye extends PCanvas {
private CalendarNode calendarNode;
public TabularFisheye() {
calendarNode = new CalendarNode();
getLayer().addChild(calendarNode);
setZoomEventHandler(null);
setPanEventHandler(null);
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent arg0) {
calendarNode.setBounds(getX(), getY(),
getWidth() - 1, getHeight() - 1);
calendarNode.layoutChildren(false);
}
});
}
}
Web Accessibility
public class TabularFisheye : PCanvas {
private CalendarNode calendar;
public TabularFisheye() {
calendar = new CalendarNode();
Layer.AddChild(calendar);
this.PanEventHandler = null;
this.ZoomEventHandler = null;
}
protected override void OnResize(EventArgs e) {
base.OnResize (e);
calendar.SetBounds(ClientRectangle.X, ClientRectangle.Y,
ClientRectangle.Width - 1, ClientRectangle.Height - 1);
calendar.LayoutChildren(false);
}
}
Web Accessibility
We turn off the default pan and zoom handlers. And
we listen to resize events to
set the bounds of the calendar node and lay out its children.
4. Add the Canvas to a Window
Now we are ready to add our new component to the window.
We create a JFrame in Java or a Form in .NET as a wrapper for our
component. Add the
following class to your project.
Java |
C#
public class TabularFisheyeTester extends JFrame {
public TabularFisheyeTester() {
setTitle("Piccolo Tabular Fisheye");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TabularFisheye tabularFisheye = new TabularFisheye();
getContentPane().add(tabularFisheye);
pack();
setVisible(true);
}
public static void main(String args[]) {
new TabularFisheyeTester();
}
}
Web Accessibility
public class TabularFisheyeTester : Form {
public TabularFisheyeTester() {
TabularFisheye tabularFisheye = new TabularFisheye();
Controls.Add(tabularFisheye);
tabularFisheye.Bounds = this.ClientRectangle;
tabularFisheye.Anchor = tabularFisheye.Anchor
| AnchorStyles.Right | AnchorStyles.Bottom;
}
static void Main() {
Application.Run(new TabularFisheyeTester());
}
}
Web Accessibility
First, we create an instance of our TabularFisheye component and add it
to the window. In the .NET version, we anchor our new
component, so that it will get resized as the window is resized. Java handles this for
us.
|