/*
* Copyright (c) 2010, Michael Grossmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the jo-widgets.org nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
package org.jowidgets.impl.widgets.composed;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import org.jowidgets.api.color.Colors;
import org.jowidgets.api.image.IconsSmall;
import org.jowidgets.api.toolkit.Toolkit;
import org.jowidgets.api.widgets.IButton;
import org.jowidgets.api.widgets.ICalendar;
import org.jowidgets.api.widgets.IComposite;
import org.jowidgets.api.widgets.ITextLabel;
import org.jowidgets.api.widgets.descriptor.setup.ICalendarSetup;
import org.jowidgets.common.types.Dimension;
import org.jowidgets.common.types.Rectangle;
import org.jowidgets.common.widgets.controller.IActionListener;
import org.jowidgets.common.widgets.controller.IInputListener;
import org.jowidgets.common.widgets.controller.IMouseEvent;
import org.jowidgets.common.widgets.layout.ILayouter;
import org.jowidgets.impl.widgets.composed.MonthComposite.IMouseoverListener;
import org.jowidgets.tools.controller.InputObservable;
import org.jowidgets.tools.controller.MouseAdapter;
import org.jowidgets.tools.widgets.blueprint.BPF;
import org.jowidgets.tools.widgets.invoker.ColorSettingsInvoker;
import org.jowidgets.tools.widgets.invoker.VisibiliySettingsInvoker;
import org.jowidgets.tools.widgets.wrapper.ControlWrapper;
import org.jowidgets.util.Assert;
public class CustomCalendarImpl extends ControlWrapper implements ICalendar {
private static final Calendar CALENDAR = new GregorianCalendar();
private static final Locale LOCALE = Locale.getDefault();
private static final int MAX_MONTH = 12;
private static final int G_X = 16;
private static final int G_Y = 8;
private static final Dimension MAX_SIZE = new Dimension(Short.MAX_VALUE, Short.MAX_VALUE);
private final InputObservable inputObservable;
private final IComposite outerComposite;
private final IComposite innerComposite;
private final MonthComposite[] monthComposites;
private final ITextLabel[] monthLabels;
private final CalendarLayouter layouter;
private final IButton navBackwardButton;
private final IButton navPrevButton;
private final IButton navNextButton;
private final IButton navForwardButton;
private final MonthLayoutCache layoutCache;
private Date date;
private MonthComposite mouseoverComposite;
public CustomCalendarImpl(final IComposite composite, final ICalendarSetup setup) {
super(composite);
this.layoutCache = new MonthLayoutCache();
this.inputObservable = new InputObservable();
this.monthComposites = new MonthComposite[MAX_MONTH];
this.monthLabels = new ITextLabel[MAX_MONTH];
this.outerComposite = composite;
this.innerComposite = outerComposite.add(BPF.composite().setBorder());
this.innerComposite.setLayout(Toolkit.getLayoutFactoryProvider().nullLayout());
composite.setBackgroundColor(Colors.WHITE);
this.layouter = new CalendarLayouter();
this.outerComposite.setLayout(layouter);
VisibiliySettingsInvoker.setVisibility(setup, this);
ColorSettingsInvoker.setColors(setup, this);
if (setup.getDate() != null) {
this.date = setup.getDate();
}
else {
this.date = new Date();
}
navBackwardButton = this.innerComposite.add(BPF.button().setIcon(IconsSmall.NAVIGATION_BACKWARD_TINY));
navNextButton = this.innerComposite.add(BPF.button().setIcon(IconsSmall.NAVIGATION_NEXT_TINY));
navPrevButton = this.innerComposite.add(BPF.button().setIcon(IconsSmall.NAVIGATION_PREVIOUS_TINY));
navForwardButton = this.innerComposite.add(BPF.button().setIcon(IconsSmall.NAVIGATION_FORWARD_TINY));
getMonthComposite(0);
navPrevButton.addActionListener(new IActionListener() {
@Override
public void actionPerformed() {
interateMonth(-1);
}
});
navNextButton.addActionListener(new IActionListener() {
@Override
public void actionPerformed() {
interateMonth(1);
}
});
navBackwardButton.addActionListener(new IActionListener() {
@Override
public void actionPerformed() {
interateYear(-1);
}
});
navForwardButton.addActionListener(new IActionListener() {
@Override
public void actionPerformed() {
interateYear(1);
}
});
this.innerComposite.addMouseListener(new MouseAdapter() {
@Override
public void mouseExit(final IMouseEvent event) {
clearMouseOvers();
}
@Override
public void mouseEnter(final IMouseEvent event) {
clearMouseOvers();
}
private void clearMouseOvers() {
if (mouseoverComposite != null) {
mouseoverComposite.clearMouseOver();
mouseoverComposite = null;
}
}
});
}
private void interateMonth(final int offset) {
this.layouter.setLayoutNeeded(true);
this.outerComposite.layoutBegin();
final Calendar calendar = new GregorianCalendar();
calendar.setTime(monthComposites[0].getDate());
calendar.add(Calendar.MONTH, offset);
for (int i = 0; i < monthComposites.length; i++) {
if (monthComposites[i] != null) {
monthComposites[i].setDate(calendar.getTime(), date);
}
if (monthLabels[i] != null) {
monthLabels[i].setText(getMonthDisplayName(calendar));
}
calendar.add(Calendar.MONTH, 1);
}
this.outerComposite.layoutEnd();
}
private void interateYear(final int offset) {
this.layouter.setLayoutNeeded(true);
this.outerComposite.layoutBegin();
final Calendar calendar = new GregorianCalendar();
calendar.setTime(monthComposites[0].getDate());
calendar.add(Calendar.YEAR, offset);
for (int i = 0; i < monthComposites.length; i++) {
if (monthComposites[i] != null) {
monthComposites[i].setDate(calendar.getTime(), date);
}
if (monthLabels[i] != null) {
monthLabels[i].setText(getMonthDisplayName(calendar));
}
calendar.add(Calendar.MONTH, 1);
}
this.outerComposite.layoutEnd();
}
@Override
public void setDate(final Date date) {
Assert.paramNotNull(date, "date");
this.layouter.setLayoutNeeded(true);
this.outerComposite.layoutBegin();
monthComposites[0].setDate(date, date);
final Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, 1);
for (int i = 1; i < monthComposites.length; i++) {
calendar.add(Calendar.MONTH, 1);
if (monthComposites[i] != null) {
monthComposites[i].setDate(calendar.getTime(), date);
}
if (monthLabels[i] != null) {
monthLabels[i].setText(getMonthDisplayName(calendar));
}
}
this.date = date;
this.outerComposite.layoutEnd();
}
@Override
public Date getDate() {
return date;
}
@Override
public void addInputListener(final IInputListener listener) {
inputObservable.addInputListener(listener);
}
@Override
public void removeInputListener(final IInputListener listener) {
inputObservable.removeInputListener(listener);
}
private MonthComposite getMonthComposite(final int index) {
if (monthComposites[index] == null) {
final IComposite monthCompositeParent = innerComposite.add(BPF.composite());
if (index == 0) {
monthComposites[index] = new MonthComposite(monthCompositeParent, this.date, date, layoutCache);
}
else {
final Date prevDate = getMonthComposite(index - 1).getDate();
final Calendar calendar = new GregorianCalendar();
calendar.setTime(prevDate);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.add(Calendar.MONTH, 1);
monthComposites[index] = new MonthComposite(monthCompositeParent, calendar.getTime(), date, layoutCache);
}
monthComposites[index].addInputListener(new IInputListener() {
@Override
public void inputChanged() {
final Date selectedDate = monthComposites[index].getSelectedDate();
if (selectedDate != null) {
for (int i = 0; i < monthComposites.length; i++) {
if (i != index && monthComposites[i] != null) {
monthComposites[i].clearSelection();
}
}
date = selectedDate;
inputObservable.fireInputChanged();
}
}
});
monthComposites[index].addMouseOverListener(new IMouseoverListener() {
@Override
public void onMouseOver() {
if (mouseoverComposite != null && mouseoverComposite != monthComposites[index]) {
mouseoverComposite.clearMouseOver();
}
mouseoverComposite = monthComposites[index];
}
});
}
return monthComposites[index];
}
private ITextLabel getMonthLabel(final int index) {
if (monthLabels[index] == null) {
final MonthComposite monthComposite = getMonthComposite(index);
final Date monthDate = monthComposite.getDate();
final Calendar calendar = new GregorianCalendar();
calendar.setTime(monthDate);
final String labelString = getMonthDisplayName(calendar);
final ITextLabel label = this.innerComposite.add(BPF.textLabel(labelString));
monthLabels[index] = label;
}
return monthLabels[index];
}
private class CalendarLayouter implements ILayouter {
private Dimension preferredSize;
private int cols;
private int cells;
private boolean layoutNeeded;
CalendarLayouter() {
layoutNeeded = true;
}
void setLayoutNeeded(final boolean layoutNeeded) {
this.layoutNeeded = layoutNeeded;
}
@Override
public void layout() {
outerComposite.setRedrawEnabled(false);
final Rectangle outerClientArea = outerComposite.getClientArea();
final Dimension lastSize = innerComposite.getSize();
final Dimension prefSize = getPreferredSize();
final boolean moreColsPossible = (lastSize.getWidth() + prefSize.getWidth() + G_X) <= outerClientArea.getWidth() + 6;
final boolean moreRowsPossible = (lastSize.getHeight() + prefSize.getHeight() + G_Y) <= outerClientArea.getHeight() + 6;
final boolean toManyCols = (lastSize.getWidth()) > outerClientArea.getWidth();
final boolean toManyRows = (lastSize.getHeight()) > outerClientArea.getHeight();
int x = 0;
int y = 0;
if (layoutNeeded
|| cells == 0
|| moreColsPossible
|| (moreRowsPossible && !(cells == MAX_MONTH))
|| toManyCols
|| toManyRows) {
layoutNeeded = false;
innerComposite.setSize(outerComposite.getClientArea().getSize());
final Rectangle clientArea = innerComposite.getClientArea();
x = clientArea.getX();
y = clientArea.getY() + G_Y;
//layout the left nav buttons
final Dimension navButtonSize = new Dimension(17, 17);
final int buttonOffsetY = (getMonthLabel(0).getPreferredSize().getHeight() - navButtonSize.getHeight()) / 2;
navBackwardButton.setSize(navButtonSize);
navBackwardButton.setPosition(x, y + buttonOffsetY);
x = x + navButtonSize.getWidth();
navPrevButton.setSize(navButtonSize);
navPrevButton.setPosition(x, y + buttonOffsetY);
//layout the months
final int prefHeight = getPreferredSize().getHeight();
x = clientArea.getX();
int maxX = x;
int maxY = y;
cols = 1;
cells = 0;
int column = 1;
for (int i = 0; i < monthComposites.length; i++) {
final ITextLabel label = getMonthLabel(i);
final MonthComposite monthComposite = getMonthComposite(i);
final Dimension monthSize = monthComposite.getPreferredSize();
final Dimension labelSize = label.getPreferredSize();
boolean layoutMonth = i == 0;
if (x + monthSize.getWidth() <= clientArea.getX() + clientArea.getWidth()) {
if (!layoutMonth) {
column++;
}
layoutMonth = true;
cols = Math.max(cols, column);
}
else if (i != 0 && y + 2 * prefHeight <= clientArea.getHeight()) {
x = clientArea.getX();
y = y + prefHeight + G_Y;
column = 1;
layoutMonth = true;
}
if (layoutMonth) {
cells++;
//layout label
final int offsetX = monthSize.getWidth() / 2 - labelSize.getWidth() / 2;
label.setSize(labelSize);
label.setPosition(x + offsetX, y);
label.setVisible(true);
final int offsetY = labelSize.getHeight() + G_Y;
//layout month composite
monthComposite.setSize(monthSize);
monthComposite.setPosition(x, y + offsetY);
monthComposite.setVisible(true);
maxX = Math.max(maxX, x + monthSize.getWidth());
x = x + monthSize.getWidth() + G_X;
maxY = y + prefHeight;
}
else {
monthComposite.setVisible(false);
label.setVisible(false);
if (i < monthComposites.length - 1 && monthComposites[i + 1] == null) {
break;
}
}
}
//layout the rigth nav buttons
y = clientArea.getY() + G_Y;
x = maxX - navButtonSize.getWidth();
navForwardButton.setSize(navButtonSize);
navForwardButton.setPosition(x, y + buttonOffsetY);
x = x - navButtonSize.getWidth();
navNextButton.setSize(navButtonSize);
navNextButton.setPosition(x, y + buttonOffsetY);
final Dimension innerMaxSize = innerComposite.computeDecoratedSize(new Dimension(maxX, maxY));
final int innerWidth = Math.min(innerMaxSize.getWidth(), outerClientArea.getWidth());
final int innerHeight = Math.min(innerMaxSize.getHeight(), outerClientArea.getHeight());
innerComposite.setSize(Math.max(prefSize.getWidth(), innerWidth), Math.max(prefSize.getHeight(), innerHeight));
}
final Dimension innerSize = innerComposite.getSize();
x = outerClientArea.getX();
y = outerClientArea.getY();
if (outerClientArea.getWidth() - innerSize.getWidth() > 2) {
x = x + (outerClientArea.getWidth() - innerSize.getWidth()) / 2;
}
if (outerClientArea.getHeight() - innerSize.getHeight() > 2) {
y = y + (outerClientArea.getHeight() - innerSize.getHeight()) / 2;
}
innerComposite.setPosition(x, y);
outerComposite.setRedrawEnabled(true);
}
@Override
public Dimension getMinSize() {
return getPreferredSize();
}
@Override
public Dimension getPreferredSize() {
if (preferredSize == null) {
preferredSize = calcPreferredSize();
}
return preferredSize;
}
@Override
public Dimension getMaxSize() {
return MAX_SIZE;
}
@Override
public void invalidate() {}
private Dimension calcPreferredSize() {
final ITextLabel label = getMonthLabel(0);
final MonthComposite monthComposite = getMonthComposite(0);
final Dimension monthSize = monthComposite.getPreferredSize();
final Dimension labelSize = label.getPreferredSize();
final int width = monthSize.getWidth();
final int height = monthSize.getHeight() + 2 * G_Y + Math.max(17, labelSize.getHeight());
return outerComposite.computeDecoratedSize(innerComposite.computeDecoratedSize(new Dimension(width, height)));
}
}
private static String getMonthDisplayName(final Calendar calendar) {
return getMonthDisplayName(calendar.get(Calendar.MONTH)) + " " + calendar.get(Calendar.YEAR);
}
private static String getMonthDisplayName(final int month) {
CALENDAR.set(Calendar.MONTH, month);
final String result = CALENDAR.getDisplayName(Calendar.MONTH, Calendar.LONG, LOCALE);
if (result == null) {
return getFallbackDisplayName(month);
}
return result;
}
private static String getFallbackDisplayName(final int month) {
if (Calendar.JANUARY == month) {
return "January";
}
else if (Calendar.FEBRUARY == month) {
return "February";
}
else if (Calendar.MARCH == month) {
return "March";
}
else if (Calendar.APRIL == month) {
return "April";
}
else if (Calendar.MAY == month) {
return "May";
}
else if (Calendar.JUNE == month) {
return "June";
}
else if (Calendar.JULY == month) {
return "July";
}
else if (Calendar.AUGUST == month) {
return "August";
}
else if (Calendar.SEPTEMBER == month) {
return "September";
}
else if (Calendar.OCTOBER == month) {
return "October";
}
else if (Calendar.NOVEMBER == month) {
return "November";
}
else if (Calendar.DECEMBER == month) {
return "December";
}
return null;
}
}