/*
* Copyright 2009 Peter Karich, peat_hal ‘at’ users ‘dot’ sourceforge ‘dot’ net.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* under the License.
*/
package de.timefinder.planner;
import de.timefinder.data.CalendarSettings;
import de.timefinder.data.Event;
import de.timefinder.data.ICalendarSettings;
import de.timefinder.data.IntervalInt;
import de.timefinder.data.IntervalLong;
import de.timefinder.data.IntervalLongImpl;
import de.timefinder.data.Task;
import de.timefinder.data.set.IntervalStepFunction;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import java.util.Date;
import java.util.logging.Logger;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.sf.nachocalendar.CalendarFactory;
import net.sf.nachocalendar.components.DateField;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
/**
* The calendar component to view events in a visual manner.
*
* TODO better project: http://sourceforge.net/projects/bizcal/ -> week view !!??
*
* Another implementation would be to let the conflicting events conflict,
* but make underlying events visible via scrollwheel and
* SwingUtilities.getDeepestComponentAt(parent, X, Y);
* or even better: http://java.sun.com/docs/books/tutorial/uiswing/components/layeredpane.html
*
* @author Peter Karich, peat_hal ‘at’ users ‘dot’ sourceforge ‘dot’ net
*/
public class TimeFinderPlanner extends JPanel {
private static final long serialVersionUID = -3997754504205811813L;
public static final String CHANGE_OBJECTS = "eventCollection";
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("TimeFinder Planner");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
CalendarSettings settings = new CalendarSettings();
// set 1/4h as one timeslot, now even half of an hour will be displayed
settings.setMillisPerTimeslot(15 * 60 * 1000L);
settings.setTimeslotsPerDay(4 * 8);
settings.setStartDate(new DateTime(2009, 4, 6, 8, 0, 0, 0));
TimeFinderPlanner planner = new TimeFinderPlanner(settings);
// monday
planner.addInterval(new IntervalLongImpl("Interval 1", 2009, 4, 6, 8, 30, 60));
planner.addInterval(new IntervalLongImpl("Interval 2", 2009, 4, 6, 9, 0, 60));
planner.addInterval(new IntervalLongImpl("Interval A", 2009, 4, 6, 10, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval B", 2009, 4, 6, 10, 0, 180));
planner.addInterval(new IntervalLongImpl("Interval C", 2009, 4, 6, 11, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval D", 2009, 4, 6, 13, 0, 180));
planner.addInterval(new IntervalLongImpl("Interval E", 2009, 4, 6, 14, 0, 120));
// tuesday
planner.addInterval(new IntervalLongImpl("Interval 3", 2009, 4, 7, 8, 30, 30));
planner.addInterval(new IntervalLongImpl("Interval 3", 2009, 4, 7, 9, 0, 60));
planner.addInterval(new IntervalLongImpl("Interval 4a", 2009, 4, 7, 10, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval 4b", 2009, 4, 7, 13, 0, 180));
planner.addInterval(new IntervalLongImpl("Interval 4c", 2009, 4, 7, 14, 0, 60));
// wednesday
planner.addInterval(new IntervalLongImpl("Interval 5", 2009, 4, 8, 9, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval 6 more description "
+ "to show line breaking", 2009, 4, 8, 10, 0, 120));
// explicit line breaking
planner.addInterval(new IntervalLongImpl("brok\nken", 2009, 4, 9, 15, 0, 30));
// implicit
planner.addInterval(new IntervalLongImpl("Interval 7 show line breaking and "
+ "clipping not shown ? 123 ********", 2009, 4, 9, 9, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval 8", 2009, 4, 9, 11, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval 9", 2009, 4, 9, 10, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval 10", 2009, 4, 9, 11, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval 11", 2009, 4, 9, 13, 0, 120));
planner.addInterval(new IntervalLongImpl("Interval 12", 2009, 4, 10, 8, 30, 60));
planner.addInterval(new IntervalLongImpl("Interval 13 smaller than one hour", 2009, 4, 10, 9, 30, 30));
frame.setContentPane(planner);
frame.setSize(1000, 640);
frame.setVisible(true);
}
});
}
private Logger logger = Logger.getLogger(getClass().getName());
// TODO: make this font independent, init from g2.getFontMetrics().getHeight(); int fontDesent = g2.getFontMetrics().getDescent();
private int xTextOffset = 4;
private int yTextOffset = 4;
private int yHeaderTextOffset = 15;
private DefaultListModel taskListModel = new DefaultListModel();
private JList taskJList = new JList(taskListModel);
private DefaultListModel eventListModel = new DefaultListModel();
private JList eventJList = new JList(eventListModel);
private JPanel rowHeader;
private JPanel columnHeader;
private JPanel timetableGrid;
private JScrollPane scroll;
private ICalendarSettings settings;
// TODO calculate from window size and initialize minSize when scrollbars should be visible
private int dayWidth = 130;
private int hourWidth = 50;
private int columnHeaderHeight = 30;
private int rowHeaderWidth = 80;
private Color gridColor = Color.LIGHT_GRAY;
private IntervalStepFunction stepFunction;
private PropertyChangeListener listener;
private int visibleDays;
private JButton nextDaysButton;
private JButton prevDaysButton;
private DateField calendarDateField;
public TimeFinderPlanner(ICalendarSettings settings_) {
this.settings = settings_;
if (settings == null) {
throw new NullPointerException("Settings cannot be null!");
}
stepFunction = new IntervalStepFunction();
timetableGrid = new ScrollablePanel() {
@Override
public boolean isOptimizedDrawingEnabled() {
// our code has no childs so they cannot overlapp
return true;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final Graphics2D g2d = (Graphics2D) g;
// get viewport clip
//Shape oldClip = g2d.getClip();
// get the whole area
//Rectangle oldBounds = getBounds();
g2d.setColor(gridColor);
// vertical lines
for (int x = 1; x < visibleDays + 1; x++) {
g2d.drawLine(x * dayWidth, 0, x * dayWidth, getHoursPerDay() * hourWidth);
}
// horizontal lines
int xEnd = visibleDays * dayWidth;
for (int y = 1; y < getHoursPerDay() + 1; y++) {
g2d.drawLine(0, y * hourWidth, xEnd, y * hourWidth);
}
RoundBox selectedBox = null;
DateTime startDate = settings.getStartDate();
// paint events
for (IntervalLong interval : getCurrentIntervals()) {
long startMillisOffset = interval.getStart() - startDate.getMillis()
+ startDate.getMillisOfDay();
int day = (int) (startMillisOffset / ICalendarSettings.DAY);
int duration_hourWidth = (int) (interval.getDuration() / settings.getMillisPerTimeslot()
* getHourFactor() * hourWidth);
int start_hourWidth = (int) ((startMillisOffset % CalendarSettings.DAY
- startDate.getMillisOfDay()) / ICalendarSettings.HOUR * hourWidth)
+ (hourWidth * interval.getStartDateTime().getMinuteOfHour() / 60);
int numberOfConflicts = stepFunction.getMaxAssignments(interval);
int position = stepFunction.getOffset(interval);
int thickness = dayWidth / numberOfConflicts;
int offset = position * thickness;
Rectangle2D paintingRect = new Rectangle2D.Double(
day * dayWidth + offset, start_hourWidth,
thickness, duration_hourWidth);
String text = interval.getDescription();
if (text == null || text.length() == 0)
text = interval.getName();
RoundBox box = new RoundBox(text, numberOfConflicts);
box.setRect(paintingRect);
if (paintingRect.intersects(mousePositionX, mousePositionY, 1, 1)) {
selectedBox = box;
continue;
}
box.paintComponent(g2d);
}
// now paint a selected interval a bit larger *and* on the top of the other
if (selectedBox != null) {
selectedBox.setTransparent(false);
selectedBox.zoom(g2d.getClip());
selectedBox.paintComponent(g2d);
}
}
};
final DateTimeFormatter fmt = new DateTimeFormatterBuilder().appendDayOfWeekText().
appendLiteral(" - ").
appendDayOfMonth(2).
appendLiteral(". ").
appendMonthOfYearText().toFormatter();
columnHeader = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Shape oldClip = g.getClip();
DateTime tempDateTime = settings.getStartDate();
for (int x = 0; x < visibleDays; x++) {
g.setClip(new Rectangle2D.Double(x * dayWidth, 0,
dayWidth, columnHeaderHeight));
g.drawString(fmt.print(tempDateTime),
xTextOffset + x * dayWidth, yHeaderTextOffset);
tempDateTime = tempDateTime.plusDays(1);
}
g.setClip(oldClip);
}
};
rowHeader = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (int y = 0; y < getHoursPerDay(); y++) {
g.drawString(String.format("%1$02d", y + getHourOffset()) + ":00",
xTextOffset, yHeaderTextOffset + y * hourWidth);
}
}
};
scroll = new JScrollPane();
scroll.setRowHeaderView(rowHeader);
// scroll.setCorner(ScrollPaneConstants.UPPER_LEFT_CORNER, new JLabel() {
//
// @Override
// public String getText() {
// return "no:" + getCurrentIntervals().size();
// }
// });
scroll.setColumnHeaderView(columnHeader);
JPanel timeNorthPanel = new JPanel();
calendarDateField = CalendarFactory.createDateField();
calendarDateField.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
// get the date only - not the time
DateTime val = new DateTime(((Date) calendarDateField.getValue()).getTime());
DateTime start = settings.getStartDate();
setStartDate(start.withYear(val.getYear()).
withMonthOfYear(val.getMonthOfYear()).withDayOfMonth(val.getDayOfMonth()));
}
});
timeNorthPanel.add(calendarDateField);
timeNorthPanel.add(prevDaysButton = new SmallButton("<"));
timeNorthPanel.add(nextDaysButton = new SmallButton(">"));
prevDaysButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setStartDate(settings.getStartDate().minusDays(settings.getNumberOfDays()));
}
});
nextDaysButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setStartDate(settings.getStartDate().plusDays(settings.getNumberOfDays()));
}
});
scroll.setViewportView(timetableGrid);
JPanel southPanel = new JPanel(new GridLayout(1, 0));
eventJList.setVisibleRowCount(3);
taskJList.setVisibleRowCount(3);
southPanel.add(new JScrollPane(eventJList));
southPanel.add(new JScrollPane(taskJList));
listener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (ICalendarSettings.CHANGE_ALL.equals(evt.getPropertyName())) {
initFromSettings();
}
}
};
settings.addListener(listener);
initFromSettings();
setLayout(new BorderLayout());
add(timeNorthPanel, BorderLayout.NORTH);
add(scroll, BorderLayout.CENTER);
add(southPanel, BorderLayout.SOUTH);
}
public void updateToolTip() {
eventJList.setToolTipText(tr("Events") + " " + eventJList.getModel().getSize());
taskJList.setToolTipText(tr("Tasks") + " " + taskJList.getModel().getSize());
}
private int getHourOffset() {
return settings.getStartDate().getHourOfDay();
}
private double getHourFactor() {
return settings.getMillisPerTimeslot() / (60 * 60 * 1000.0);
}
private int getHoursPerDay() {
return (int) (settings.getTimeslotsPerDay() * getHourFactor()) + 1;
}
public void setStartDate(DateTime dateTime) {
settings.setStartDate(dateTime);
initFromSettings();
// preferred size could have changed => repaint is not sufficient
revalidate();
repaint();
}
public boolean addObject(Object obj) {
if (obj instanceof Event) {
Event ev = (Event) obj;
if (ev.getStart() < 0)
addTask(settings.toTask(ev));
else
addInterval(settings.toIntervalLong(ev));
} else if (obj instanceof IntervalInt) {
IntervalInt i = (IntervalInt) obj;
return addInterval(settings.toIntervalLong(i));
} else if (obj instanceof Task) {
addTask((Task) obj);
return true;
}
return false;
}
public boolean addInterval(IntervalLong interval) {
if (containsInterval(interval)) {
logger.severe("Couldn't add interval: " + interval);
return false;
}
// logger.fine("Adding interval:" + interval);
eventListModel.addElement(interval);
boolean ret = stepFunction.addInterval(interval);
if (!ret)
logger.severe("Couldn't add interval:" + interval + " to stepFunction - " + stepFunction);
updateToolTip();
repaint();
return true;
}
public void addAllInterval(Collection<? extends IntervalLong> all) {
for (IntervalLong ev : all) {
addInterval(ev);
}
}
public void addTask(Task task) {
// logger.fine("Adding task:" + task);
taskListModel.addElement(task);
updateToolTip();
}
public void addAllTasks(Collection<Task> all) {
for (Task t : all) {
addTask(t);
}
}
public boolean removeInterval(IntervalLong ev) {
boolean ret = containsInterval(ev);
if (ret) {
eventListModel.addElement(ev);
stepFunction.removeInterval(ev);
repaint();
}
return ret;
}
public void removeAllObjects() {
// logger.fine("Remove all objects");
stepFunction.clear();
eventListModel.clear();
repaint();
}
public void removeAllTasks() {
taskListModel.clear();
}
public boolean containsInterval(IntervalLong interval) {
return stepFunction.containsInterval(interval);
}
public Collection<IntervalLong> getCurrentIntervals() {
long start = settings.getStartDate().getMillis();
long duration = settings.getNumberOfDays() * ICalendarSettings.DAY;
return stepFunction.getIntervals(start, start + duration);
}
@Override
public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
super.firePropertyChange(propertyName, oldValue, newValue);
if (propertyName.equals(CHANGE_OBJECTS)) {
removeAllObjects();
removeAllTasks();
for (Object o : (Collection) newValue) {
addObject(o);
}
}
}
public void initFromSettings() {
if (getHoursPerDay() < 2 || getHourFactor() == 0 || getHourOffset() < 0) {
logger.severe("startup configuration was wrong!");
return;
}
calendarDateField.setValue(settings.getStartDate().toDate());
visibleDays = settings.getNumberOfDays();
Dimension prefSize = new Dimension(visibleDays * dayWidth,
getHoursPerDay() * hourWidth);
rowHeader.setPreferredSize(new Dimension(rowHeaderWidth, prefSize.height));
columnHeader.setPreferredSize(new Dimension(prefSize.width, columnHeaderHeight));
timetableGrid.setMaximumSize(prefSize);
timetableGrid.setPreferredSize(prefSize);
}
/**
* This Method releases all resources associated with this object.
*/
public void close() {
boolean ret = settings.removeListener(listener);
assert ret == true;
}
@Override
public void repaint() {
super.repaint();
if (timetableGrid != null) {
timetableGrid.repaint();
}
}
@Override
public void revalidate() {
super.revalidate();
// after init timetableGrid != null and should be revalidated as well,
// because of the possible changed pref-size.
if (timetableGrid != null) {
timetableGrid.revalidate();
}
}
/**
* This method translates the specified field into a localized string.
* Go through the code to locate the usage.
*/
public String tr(String str) {
return str;
}
}
/**
* Gimmick class, so that scrolling is a bit faster.
*/
class ScrollablePanel extends JPanel implements Scrollable, MouseMotionListener, MouseListener {
private static final long serialVersionUID = 3475577056164274369L;
private int block = 50;
private int unit = 25;
protected int mousePositionX = -10;
protected int mousePositionY = -10;
public ScrollablePanel() {
addMouseMotionListener(this);
addMouseListener(this);
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return unit;
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return block;
}
@Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
@Override
public void mouseDragged(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
mousePositionX = e.getX();
mousePositionY = e.getY();
repaint();
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
// avoid that zoomed intervals stays zoomed
mousePositionX = -10;
mousePositionY = -10;
repaint();
}
}