/*******************************************************************************
* Copyright (c) Emil Crumhorn - Hexapixel.com - emil.crumhorn@gmail.com
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* emil.crumhorn@gmail.com - initial API and implementation
*******************************************************************************/
package org.eclipse.nebula.widgets.calendarcombo;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.DragDetectListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.HelpListener;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Widget;
/**
* <b>CalendarCombo - SWT Widget - 2005-2008. Version 1.0. © Emil Crumhorn - emil dot crumhorn at gmail dot
* com.</b>
* <p>
* <b>Website</b><br>
* If you want more info or more documentation, please visit: <a
* href="http://www.hexapixel.com/">http://www.hexapixel.com</a>
* <p>
* <b>Description</b><br>
* CalendarCombo is a widget that opens a calendar when dropped down. The calendar is modelled after Microsoft Outlook's
* calendar widget and acts and behaves exactly the same (and it is also theme based). The combo is not based on CCombo
* (as many other custom implementations), but is instead attached to the native Combo box.
* <p>
* <b>Example Creation Code</b><br>
* <code>
* CalendarCombo cCombo = new CalendarCombo(parentControl, SWT.READ_ONLY);<br>
* ...<br>
* </code>
* <p>
* <b>Another example using depending combos and date range selection on the first combo</b><br>
* <code>
* CalendarCombo cComboStart = new CalendarCombo(parentControl, SWT.READ_ONLY, true);<br>
* CalendarCombo cComboEnd = new CalendarCombo(parentControl, SWT.READ_ONLY);<br>
* cComboStart.setDependingCombo(cComboEnd);<br>
* </code> <br>
* This will cause the end date for the date range selection to be populated in the cComboEnd combo.
* <p>
* <b>Customizing</b><br>
* There are two interfaces that are of importance for customizing, one is IColorManager and the other is ISettings.
* Let's start with the IColorManager.
* <p>
* <b>IColorManager</b><br>
* If you don't specify a color manager, the DefaultColorManager will be used. The color manager's job is to return
* colors to the method that is painting all the actual days and months etc. The colors that are returned from the
* ColorManager will determine everything as far as looks go.
* <p>
* <b>ISettings</b><br>
* To control the spacing between dates, various formats and text values, you will want to implement the ISettings
* interface. If you don't specify one, DefaultSettings will be used.
*
* @author Emil Crumhorn
* @version 1.1.2008.11.25
*/
public class CalendarCombo extends Composite {
// the main combo box
private Combo mCombo;
private FlatCalendarCombo mFlatCombo;
private Composite mComboControl;
private int mComboStyle = SWT.NONE;
// the shell holding the CalendarComposite
private Shell mCalendarShell;
private Listener mKillListener;
private Listener mFilterListenerFocusIn;
private Composite mParentComposite;
// values for determining when the last view of the mCalendarShell was.
// this is to determine how quick clicks should behave
private long mLastShowRequest = 0;
private long mLastKillRequest = 0;
private CalendarCombo mDependingCombo;
private CalendarComposite mCalendarComposite;
private Calendar mStartDate;
private Calendar mEndDate;
private IColorManager mColorManager;
private ISettings mSettings;
private ArrayList mListeners;
private Calendar mDisallowBeforeDate;
private Calendar mDisallowAfterDate;
private boolean isReadOnly;
private boolean isFlat;
private int arrowButtonWidth;
private boolean mAllowDateRange;
private Calendar mCarbonPrePopupDate;
private Listener mKeyDownListener;
private boolean mParsingDate;
private int mLastFireTime;
private Calendar mLastNotificationDate;
private Listener mOobClickListener;
private List mDateParseExceptionListeners;
private Listener mOobDisplayFilterListener;
protected static final boolean OS_CARBON = "carbon".equals(SWT.getPlatform());
protected static final boolean OS_GTK = "gtk".equals(SWT.getPlatform());
protected static final boolean OS_WINDOWS = "win32".equals(SWT.getPlatform());
private Listener shellDeactivate;
/*
* // Windows JNI Code for making a non-activated canvas, for reference if
* searching MSDN int extStyle = OS.GetWindowLong(canvas.handle,
* OS.GWL_EXSTYLE); extStyle = extStyle | 0x80000;
* OS.SetWindowLong(canvas.handle, OS.GWL_EXSTYLE, extStyle); 0x008000000 =
* WS_EX_NOACTIVATE;
*/
/**
* Creates a new calendar combo box with the given style.
*
* @param parent Parent control
* @param style Combo style
*/
public CalendarCombo(Composite parent, int style) {
super(parent, checkStyle(style));
this.mComboStyle = style;
this.mParentComposite = parent;
init();
}
/**
* Creates a new calendar combo box with the given style.
*
* @param parent Parent control
* @param style Combo style
* @param allowDateRange Whether to allow date range selection (note that if there is no depending CalendarCombo set
* you will have to deal with the date range event yourself).
*/
public CalendarCombo(Composite parent, int style, boolean allowDateRange) {
super(parent, checkStyle(style));
this.mComboStyle = style;
this.mParentComposite = parent;
this.mAllowDateRange = allowDateRange;
init();
}
/**
* Creates a new calendar mCombo box with the given style, ISettings and IColorManager implementations.
*
* @param parent Parent control
* @param style Combo style
* @param settings ISettings implementation
* @param colorManager IColorManager implementation
*/
public CalendarCombo(Composite parent, int style, ISettings settings, IColorManager colorManager) {
super(parent, checkStyle(style));
this.mComboStyle = style;
this.mParentComposite = parent;
this.mSettings = settings;
this.mColorManager = colorManager;
init();
}
/**
* Creates a new calendar mCombo box with the given style, ISettings and IColorManager implementations.
*
* @param parent Parent control
* @param style Combo style
* @param settings ISettings implementation
* @param colorManager IColorManager implementation
* @param allowDateRange Whether to allow date range selection (note that if there is no depending CalendarCombo set
* you will have to deal with the date range event yourself).
*/
public CalendarCombo(Composite parent, int style, ISettings settings, IColorManager colorManager, boolean allowDateRange) {
super(parent, checkStyle(style));
this.mComboStyle = style;
this.mParentComposite = parent;
this.mSettings = settings;
this.mColorManager = colorManager;
this.mAllowDateRange = allowDateRange;
init();
}
/**
* Lets you create a depending CalendarCombo box, which, when no dates are set on the current one will set the
* starting date when popped up to be the date of the pullDateFrom CalendarCombo, should that box have a date set in
* it.
*
* @param parent
* @param style
* @param dependingCombo CalendarCombo from where start date is pulled when no date has been set locally.
*/
public CalendarCombo(Composite parent, int style, CalendarCombo dependingCombo) {
super(parent, checkStyle(style));
this.mParentComposite = parent;
this.mDependingCombo = dependingCombo;
this.mComboStyle = style;
init();
}
/**
* Lets you create a depending CalendarCombo box, which, when no dates are set on the current one will set the
* starting date when popped up to be the date of the pullDateFrom CalendarCombo, should that box have a date set in
* it. When the depending combo is set and the allowDateRange flag is true, the depending combo will be the
* recipient of the end date of any date range selection.
*
* @param parent
* @param style
* @param dependingCombo CalendarCombo from where start date is pulled when no date has been set locally.
* @param allowDateRange Whether to allow date range selection
*/
public CalendarCombo(Composite parent, int style, CalendarCombo dependingCombo, boolean allowDateRange) {
super(parent, checkStyle(style));
this.mParentComposite = parent;
this.mDependingCombo = dependingCombo;
this.mComboStyle = style;
this.mAllowDateRange = allowDateRange;
init();
}
/**
* Lets you create a depending CalendarCombo box, which, when no dates are set on the current one will set the
* starting date when popped up to be the date of the pullDateFrom CalendarCombo, should that box have a date set in
* it.
*
* @param parent
* @param style
* @param dependingCombo CalendarCombo from where start date is pulled when no date has been set locally.
* @param settings ISettings implementation
* @param colorManager IColorManager implementation
*/
public CalendarCombo(Composite parent, int style, CalendarCombo dependingCombo, ISettings settings, IColorManager colorManager) {
super(parent, checkStyle(style));
this.mParentComposite = parent;
this.mDependingCombo = dependingCombo;
this.mComboStyle = style;
this.mSettings = settings;
this.mColorManager = colorManager;
init();
}
/**
* Lets you create a depending CalendarCombo box, which, when no dates are set on the current one will set the
* starting date when popped up to be the date of the pullDateFrom CalendarCombo, should that box have a date set in
* it. When the depending combo is set and the allowDateRange flag is true, the depending combo will be the
* recipient of the end date of any date range selection.
*
* @param parent
* @param style
* @param dependingCombo CalendarCombo from where start date is pulled when no date has been set locally.
* @param settings ISettings implementation
* @param colorManager IColorManager implementation
* @param allowDateRange Whether to allow date range selection
*/
public CalendarCombo(Composite parent, int style, CalendarCombo dependingCombo, ISettings settings, IColorManager colorManager, boolean allowDateRange) {
super(parent, checkStyle(style));
this.mParentComposite = parent;
this.mDependingCombo = dependingCombo;
this.mComboStyle = style;
this.mSettings = settings;
this.mColorManager = colorManager;
init();
}
// remove styles we don't allow
private static int checkStyle(int style) {
int mask = SWT.H_SCROLL | SWT.V_SCROLL | SWT.SINGLE | SWT.MULTI | SWT.NO_FOCUS | SWT.CHECK | SWT.VIRTUAL | SWT.FLAT;
int newStyle = style & mask;
return newStyle;
}
// lay out everything and add all our listeners
private void init() {
isReadOnly = ((mComboStyle & SWT.READ_ONLY) != 0);
isFlat = ((mComboStyle & SWT.FLAT) != 0);
mListeners = new ArrayList();
mDateParseExceptionListeners = new ArrayList();
// if click happens on a control that is not us, we kill
mOobClickListener = new Listener() {
public void handleEvent(Event event) {
if (!isCalendarVisible()) return;
Control cc = getDisplay().getCursorControl();
if (cc != mCalendarComposite) {
if (cc != mCalendarShell) {
boolean killIt = false;
if (isFlat) {
if (cc != mFlatCombo.getTextControl() && cc != mFlatCombo.getArrowButton()) {
killIt = true;
}
} else {
if (cc != mCombo) {
killIt = true;
}
}
if (killIt) {
kill(44);
}
}
}
}
};
if (mColorManager == null) mColorManager = new DefaultColorManager();
if (mSettings == null) mSettings = new DefaultSettings();
arrowButtonWidth = mSettings.getWindowsButtonWidth();
if (OS_CARBON) arrowButtonWidth = mSettings.getCarbonButtonWidth();
else if (OS_GTK) arrowButtonWidth = mSettings.getGTKButtonWidth();
GridLayout gl = new GridLayout();
gl.horizontalSpacing = 0;
gl.verticalSpacing = 0;
gl.marginWidth = 0;
gl.marginHeight = 0;
setLayout(gl);
if (isFlat) {
mFlatCombo = new FlatCalendarCombo(this, this, mComboStyle | SWT.FLAT);
mFlatCombo.setVisibleItemCount(0);
mFlatCombo.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
mComboControl = mFlatCombo;
} else {
mCombo = new Combo(this, mComboStyle);
mCombo.setVisibleItemCount(0);
mCombo.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
mComboControl = mCombo;
}
// when a user types in a date we parse it when they traverse away in
// any sense or form (focus lost etc)
if (!isReadOnly) {
Listener traverseListener = new Listener() {
public void handleEvent(Event event) {
// double event, ignore
if (event.time == mLastFireTime) { return; }
if (event.detail == 16 || event.detail == 8 || event.detail == 4 || event.detail == 0) {
parseTextDate(true);
}
mLastFireTime = event.time;
}
};
// deal with traverse-away/in/return events to parse dates
mComboControl.addListener(SWT.Traverse, traverseListener);
mComboControl.addListener(SWT.FocusOut, traverseListener);
mKeyDownListener = new Listener() {
public void handleEvent(Event event) {
// if event didn't happen on this combo, ignore it
if (isFlat) {
if (event.widget != mFlatCombo.getTextControl()) return;
} else {
if (event.widget != mCombo) { return; }
}
if (mSettings.keyboardNavigatesCalendar()) {
if (event.keyCode == SWT.ARROW_DOWN) {
Control ctrl = (isFlat ? (Control) mFlatCombo.getTextControl() : mCombo);
if (getDisplay().getFocusControl() == ctrl) {
if (!isCalendarVisible()) showCalendar();
else {
mCalendarComposite.keyPressed(event.keyCode, event.stateMask);
event.doit = false;
}
}
} else {
if (isCalendarVisible()) {
mCalendarComposite.keyPressed(event.keyCode, event.stateMask);
// eat event or cursor will jump around in combo
// as well
event.doit = false;
}
}
} else {
boolean acceptedEvent = (event.keyCode == SWT.ARROW_DOWN || event.keyCode == SWT.ARROW_UP);
if (OS_CARBON) {
acceptedEvent = (event.character == mSettings.getCarbonArrowDownChar() || event.character == mSettings.getCarbonArrowUpChar());
}
if (acceptedEvent) {
boolean up = event.keyCode == SWT.ARROW_UP;
if (OS_CARBON) up = event.character == mSettings.getCarbonArrowUpChar();
int cursorLoc = isFlat ? mFlatCombo.getSelection().x : mCombo.getSelection().x;
// first, parse the date, we don't care if it's some
// fantastic format we can parse, parse it again
parseTextDate(true);
// once it's parsed, set it to the default format,
// that way we KNOW where certain parts of the date
// are
if (mStartDate != null) {
setComboText(DateHelper.getDate(mStartDate, mSettings.getDateFormat()));
String df = mSettings.getDateFormat();
event.doit = false;
if (isFlat) mFlatCombo.setSelection(new Point(cursorLoc, cursorLoc));
else mCombo.setSelection(new Point(cursorLoc, cursorLoc));
// split the date format. we do this as a date
// format of M/d/yyyy for example can still have
// 2 digits as M or d .
String separatorChar = null;
char[] accepted = mSettings.getAcceptedDateSeparatorChars();
for (int i = 0; i < accepted.length; i++) {
if (df.indexOf(String.valueOf(accepted[i])) > -1) {
separatorChar = String.valueOf(accepted[i]);
}
}
int sectionStart = 0;
int sectionEnd = 0;
int splitCount = 0;
// get the format
String oneChar = "";
if (separatorChar != null) {
// now find how many separator chars we are
// from the left side of the date in the box
// to where the cursor is, that will tell us
// what part of
// the date format we're on
String comboText = isFlat ? mFlatCombo.getText() : mCombo.getText();
for (int i = 0; i < comboText.length(); i++) {
if (i >= cursorLoc) break;
if (comboText.charAt(i) == separatorChar.charAt(0)) splitCount++;
}
StringTokenizer st = new StringTokenizer(df, separatorChar);
int count = 0;
while (st.hasMoreTokens()) {
String tok = st.nextToken();
if (count == splitCount) {
oneChar = tok;
break;
}
count++;
}
} else {
oneChar = mSettings.getDateFormat().substring(cursorLoc, cursorLoc + 1);
// get the whole part, bit tricky I suppose,
// but
// we fetch everything that matches the
// format
// at the position we're at, easy enough
StringBuffer buf = new StringBuffer();
int start = cursorLoc;
while (start >= 0) {
if (mSettings.getDateFormat().charAt(start) == oneChar.charAt(0)) {
buf.append(mSettings.getDateFormat().charAt(start));
} else {
break;
}
start--;
}
start = cursorLoc + 1;
while (start < mSettings.getDateFormat().length()) {
if (mSettings.getDateFormat().charAt(start) == oneChar.charAt(0)) {
buf.append(mSettings.getDateFormat().charAt(start));
} else {
break;
}
start++;
}
sectionStart = mSettings.getDateFormat().indexOf(buf.toString());
sectionEnd = sectionStart + buf.toString().length();
}
// Korean dates and some others have spaces in
// them (!?)
oneChar = oneChar.replaceAll(" ", "");
if (oneChar.length() == 0) return;
// now we now what to increase/decrease, lets do
// it
int calType = DateHelper.getCalendarTypeForString(oneChar);
if (calType != -1) {
mStartDate.add(calType, up ? 1 : -1);
String newDate = DateHelper.getDate(mStartDate, mSettings.getDateFormat());
setComboText(newDate);
if (separatorChar != null) {
// we need to update the selection after
// we've set the date
// figure out cursor location, now we
// have to use the date in the box again
StringTokenizer st = new StringTokenizer(newDate, separatorChar);
int count = 0;
boolean stop = false;
while (st.hasMoreTokens()) {
String tok = st.nextToken();
// we found our section
if (count == splitCount) {
sectionEnd = sectionStart + tok.length();
stop = true;
break;
}
// if we're stopping, break out
if (stop) break;
// add on separator chars for each
// loop iteration post 0
sectionStart += 1;
// and token length
sectionStart += tok.length();
count++;
}
}
// set the selection for us
if (isFlat) {
mFlatCombo.setSelection(new Point(sectionStart, sectionEnd));
} else {
mCombo.setSelection(new Point(sectionStart, sectionEnd));
}
}
}
}
}
}
};
getDisplay().addFilter(SWT.KeyDown, mKeyDownListener);
}
if (isFlat) {
mComboControl.addListener(SWT.FocusOut, new Listener() {
public void handleEvent(Event event) {
kill(98);
}
});
}
mComboControl.addListener(SWT.MouseDown, new Listener() {
public void handleEvent(Event event) {
// click in the text area? ignore
if (!isFlat) {
if (isTextAreaClick(event)) {
if (isCalendarVisible()) {
kill(16);
}
return;
}
}
// kill calendar if visible and do nothing else
if (isCalendarVisible()) {
kill(15);
return;
}
// a very small time between the close and the open means it was
// a click on the
// arrow down to close, and we don't open it again in that case.
// The next click will open.
// this is the expected behavior.
mLastShowRequest = Calendar.getInstance(mSettings.getLocale()).getTimeInMillis();
long diff = mLastKillRequest - mLastShowRequest;
if (diff > -100 && diff < 0) { return; }
showCalendar();
}
});
mKillListener = new Listener() {
public void handleEvent(Event event) {
if (event.keyCode == SWT.ESC) {
kill(77);
return;
}
// ignore arrow down events for killing popup
if (event.keyCode == SWT.ARROW_DOWN || event.keyCode == SWT.ARROW_UP || event.keyCode == SWT.ARROW_LEFT || event.keyCode == SWT.ARROW_RIGHT || event.keyCode == SWT.CR || event.keyCode == SWT.LF) { return; }
kill(1);
}
};
int[] comboEvents = { SWT.Dispose, SWT.Move, SWT.Resize };
for (int i = 0; i < comboEvents.length; i++) {
this.addListener(comboEvents[i], mKillListener);
}
int[] arrowEvents = {
//SWT.Selection
};
for (int i = 0; i < arrowEvents.length; i++) {
mComboControl.addListener(arrowEvents[i], mKillListener);
}
mFilterListenerFocusIn = new Listener() {
public void handleEvent(Event event) {
if (OS_CARBON) {
Widget widget = event.widget;
if (widget instanceof CalendarComposite == false) kill(2);
// on mac, select all text in combo if we are the control
// that gained focus
if (mComboControl == getDisplay().getFocusControl()) {
if (isFlat) {
mFlatCombo.getTextControl().selectAll();
} else {
mCombo.setSelection(new Point(0, mCombo.getText().length()));
}
}
} else {
long now = Calendar.getInstance(mSettings.getLocale()).getTimeInMillis();
long diff = now - mLastShowRequest;
if (diff > 0 && diff < 100) return;
if (!isCalendarVisible()) return;
// don't force focus, user clicked another control, let it
// grab the focus or it'll be odd behavior
if (!isFlat) kill(3, true);
}
}
};
final Shell parentShell = mParentComposite.getShell();
if (parentShell != null) {
parentShell.addControlListener(new ControlListener() {
public void controlMoved(ControlEvent e) {
kill(4);
}
public void controlResized(ControlEvent e) {
kill(5);
}
});
shellDeactivate = new Listener() {
public void handleEvent(Event event) {
// with no focus shells, buttons will steal focus, and cause
// deactivate events when clicked
// so if the deactivate came from a mouse being over any of
// our buttons, that is the same as
// if we clicked them.
if (mCalendarComposite != null && mCalendarComposite.isDisposed() == false) {
mCalendarComposite.externalClick(getDisplay().getCursorLocation());
}
if (!isFlat) kill(6);
}
};
parentShell.addListener(SWT.Deactivate, shellDeactivate);
// new Listener() {
// public void handleEvent(Event event) {
// Point mouseLoc = getDisplay().getCursorLocation();
//
// // with no focus shells, buttons will steal focus, and cause
// // deactivate events when clicked
// // so if the deactivate came from a mouse being over any of
// // our buttons, that is the same as
// // if we clicked them.
// if (mCalendarComposite != null && mCalendarComposite.isDisposed() == false) mCalendarComposite.externalClick(mouseLoc);
//
// if (!isFlat) kill(6);
// }
// });
parentShell.addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {
}
public void focusLost(FocusEvent e) {
kill(7);
}
});
}
mOobDisplayFilterListener = new Listener() {
public void handleEvent(Event event) {
// This may seem odd, but if the event is the CalendarCombo, we
// actually clicked outside the widget and the CalendarComposite
// area, meaning -
// we clicked outside our widget, so we close it.
if (event.widget instanceof CalendarCombo) {
kill(8);
mComboControl.setFocus();
}
}
};
getDisplay().addFilter(SWT.MouseDown, mOobDisplayFilterListener);
// remove listener when mCombo is disposed
getDisplay().addFilter(SWT.FocusIn, mFilterListenerFocusIn);
mComboControl.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent event) {
parentShell.removeListener(SWT.Deactivate, shellDeactivate);
getDisplay().removeFilter(SWT.FocusIn, mFilterListenerFocusIn);
getDisplay().removeFilter(SWT.MouseDown, mOobDisplayFilterListener);
if (mKeyDownListener != null) getDisplay().removeFilter(SWT.KeyDown, mKeyDownListener);
if (mSettings.getCarbonDrawFont() != null) mSettings.getCarbonDrawFont().dispose();
if (mSettings.getWindowsMonthPopupDrawFont() != null) mSettings.getWindowsMonthPopupDrawFont().dispose();
}
});
// mac's editable combos behave differently
if (OS_CARBON) {
// this code seems obsolete as of June 24th 2008, Allow text input
// on Mac, we parse it anyways now, which we didn't do prior
// leaving code in for a while to remind myself
/*
* mCombo.addVerifyListener(new VerifyListener() { public void
* verifyText(VerifyEvent event) { if (isCalendarVisible() ||
* mAllowTextEntry) { ; } else { event.doit = false; } } });
*/
// this is the most messed up thing ever, but it works. Basically,
// OSX will pop up a combo of 1 item to let you pick it
// even when it's blantantly obvious that you can only pick that one
// item.. duh.. Anyway, the Paint event actually fires PRIOR to the
// combo
// opening, and our "cure" to the popup issue is to quickly remove
// the item from the combo before it does open, thus, it doesn't
// show the selector list,
// but our popup instead. The side effect is that the combo appears
// blank while the calendar is open. But as we reset the date when
// no selection has been made, it's all good! It ain't pretty, but
// it does the job
if (isReadOnly) {
if (isFlat) {
mFlatCombo.addListener(SWT.Paint, new Listener() {
public void handleEvent(Event event) {
mCarbonPrePopupDate = getDate();
// mFlatCombo.removeAll();
}
});
} else {
mCombo.addListener(SWT.Paint, new Listener() {
public void handleEvent(Event event) {
mCarbonPrePopupDate = getDate();
mCombo.removeAll();
}
});
}
}
}
parentShell.addShellListener(new ShellListener() {
public void shellActivated(ShellEvent event) {
}
public void shellClosed(ShellEvent event) {
// shell killed
kill(9);
}
public void shellDeactivated(ShellEvent event) {
}
public void shellDeiconified(ShellEvent event) {
}
public void shellIconified(ShellEvent event) {
// shell no longer in focus (like.. WindowsKey + M on WinXP or
// such)
kill(10);
}
});
}
// parses the date, and really tries
private void parseTextDate(boolean notifyListeners) {
// listeners by users can cause infinite loops as we notify, they set dates on
// notify, etc, so don't allow parsing if we haven't finished internally yet.
if (mParsingDate) { return; }
try {
mParsingDate = true;
String comboText = (isFlat ? mFlatCombo.getText() : mCombo.getText());
if (comboText.length() == 0 && mStartDate != null) {
mStartDate = null;
setText("");
if (mLastNotificationDate != null && notifyListeners) {
notifyDateChangedToNull();
}
return;
}
mStartDate = DateHelper.parse(comboText, mSettings.getLocale(), mSettings.getDateFormat(), mSettings.getAcceptedDateSeparatorChars(), mSettings.getAdditionalDateFormats());
updateDate();
if (notifyListeners) {
notifyDateChanged();
}
mParsingDate = false;
} catch (CalendarDateParseException dpe) {
if (!mDateParseExceptionListeners.isEmpty()) {
notifyDateParseException(dpe);
} else {
dpe.printStackTrace();
}
} catch (Exception err) {
err.printStackTrace();
} finally {
mParsingDate = false;
}
}
private void notifyDateParseException(CalendarDateParseException dpe) {
for (int i = 0; i < mDateParseExceptionListeners.size(); i++) {
((IDateParseExceptionListener) mDateParseExceptionListeners.get(i)).parseExceptionThrown(dpe);
}
}
private void notifyDateChangedToNull() {
if (mLastNotificationDate == null) { return; }
for (int i = 0; i < mListeners.size(); i++) {
try {
((ICalendarListener) mListeners.get(i)).dateChanged(mStartDate);
} catch (Exception err) {
err.printStackTrace();
}
}
mLastNotificationDate = null;
}
private void notifyDateRangeChanged() {
for (int i = 0; i < mListeners.size(); i++) {
try {
((ICalendarListener) mListeners.get(i)).dateRangeChanged(mStartDate, mEndDate);
} catch (Exception err) {
err.printStackTrace();
}
}
}
private void notifyDateChanged() {
if (mStartDate == null) {
notifyDateChangedToNull();
return;
}
if (mLastNotificationDate != null && mStartDate != null) {
if (DateHelper.sameDate(mLastNotificationDate, mStartDate)) { return; }
}
mLastNotificationDate = (Calendar) mStartDate.clone();
for (int i = 0; i < mListeners.size(); i++) {
try {
((ICalendarListener) mListeners.get(i)).dateChanged(mStartDate);
} catch (Exception err) {
err.printStackTrace();
}
}
}
// checks whether a click was actually in the text area of a combo and not
// on the arrow button. This is a hack by all means,
// as there is (currently) no way to get the actual button from a combo.
private boolean isTextAreaClick(Event event) {
// read-only combos open on click anywhere
if (isReadOnly) return false;
Point size = isFlat ? mFlatCombo.getSize() : mCombo.getSize();
Rectangle rect = null;
rect = new Rectangle(0, 0, size.x - arrowButtonWidth, size.y);
if (isInside(event.x, event.y, rect)) return true;
return false;
}
// check whether a pixel value is inside a rectangle
private boolean isInside(int x, int y, Rectangle rect) {
if (rect == null) return false;
return x >= rect.x && y >= rect.y && x <= (rect.x + rect.width) && y <= (rect.y + rect.height);
}
/**
* Sets the current date. Date will be automatically displayed in the text area of the combo according to the
* defined date format (in settings).
*
* @param date Date to set
*/
public synchronized void setDate(Date date) {
checkWidget();
if (date == null) {
clear();
} else {
Calendar cal = Calendar.getInstance(mSettings.getLocale());
cal.setTime(date);
setDate(cal);
}
}
/**
* Sets the current date. Date will be automatically displayed in the text area of the combo according to the
* defined date format (in settings).
*
* @param cal Calendar to set
*/
public synchronized void setDate(Calendar cal) {
checkWidget();
mStartDate = cal;
updateDate();
}
/**
* Sets the text in the combo area to the given value. Do note that if you set a text that is a non-pareseable date
* string according to the currently set date format, that string will be replaced or removed when the user opens
* the popup. This is mainly for disabled combos or combos where you need to add additional control to what's
* displayed in the text area.
*
* @param text Text to display
*/
public synchronized void setText(final String text) {
checkWidget();
String txt = isFlat ? mFlatCombo.getText() : mCombo.getText();
if (txt.equals(text)) return;
setComboText(text);
}
private void setComboText(String text) {
if (isFlat) {
// mFlatCombo.removeAll();
mFlatCombo.setText(text);
// mFlatCombo.select(0);
} else {
mCombo.removeAll();
mCombo.add(text);
mCombo.select(0);
}
}
private synchronized void kill(int debug) {
kill(debug, false);
}
// kills the popup area and unhooks various listeners, takes an integer so
// that we can debug where the close comes from easier
private synchronized void kill(int debug, boolean skipFocus) {
if (mCalendarComposite == null) return;
if (mCalendarComposite.isDisposed()) return;
if (mCalendarComposite != null && mCalendarComposite.isMonthPopupActive()) return;
// System.err.println(debug);
if (mCalendarShell != null && !mCalendarShell.isDisposed()) {
mLastKillRequest = Calendar.getInstance(mSettings.getLocale()).getTimeInMillis();
mCalendarShell.setVisible(false);
mCalendarShell.dispose();
}
getDisplay().removeFilter(SWT.KeyDown, mKillListener);
getDisplay().removeFilter(SWT.MouseDown, mOobClickListener);
getDisplay().removeFilter(SWT.MouseDown, mOobDisplayFilterListener);
if (mComboControl != null && !mComboControl.isDisposed()) {
mComboControl.setCapture(false);
// have to traverse escape key after the fake popup is closed or the
// "0 length" popup will have focus
// traversing forces it to close
mComboControl.traverse(SWT.TRAVERSE_ESCAPE);
// if (getDate().length() != 0)
// setText(DateHelper.getFormattedDate(DateHelper.getDate(getDate(),
// mDateFormat), mDateFormat));
}
if (OS_CARBON) {
if (mCarbonPrePopupDate != null) setDate(mCarbonPrePopupDate);
}
if (!skipFocus) {
if (isFlat) {
mFlatCombo.getTextControl().setFocus();
} else {
mCombo.setFocus();
}
}
}
private boolean isCalendarVisible() {
/* try {
throw new Exception();
}
catch (Exception err) {
err.printStackTrace();
}
*/return (mCalendarShell != null && !mCalendarShell.isDisposed());
}
/**
* Pops open the calendar popup.
*/
public void openCalendar() {
checkWidget();
if (isCalendarVisible()) { return; }
showCalendar();
}
/**
* Closes the calendar popup.
*/
public void closeCalendar() {
checkWidget();
if (!isCalendarVisible()) { return; }
kill(99);
}
/**
* Returns the set date as the raw String representation that is currently displayed in the combo.
*
* @return String date
*/
public String getDateAsString() {
checkWidget();
String text = isFlat ? mFlatCombo.getText() : mCombo.getText();
if (text.equals("")) return "";
return text;
}
/**
* Returns the currently set date.
*
* @return Calendar of date selection or null.
*/
public Calendar getDate() {
checkWidget();
if (!isReadOnly) {
parseTextDate(false);
}
return mStartDate;
}
/*
* private void setDateBasedOnComboText() { try { Date d =
* DateHelper.getDate(isFlat ? mFlatCombo.getText() : mCombo.getText(),
* mSettings.getDateFormat()); setDate(d); } catch (Exception err) { // we
* don't care } }
*/
// shows the calendar area
public synchronized void showCalendar() {
try {
// this is part of the OSX issue where the popup for a combo is
// shown even if there is only 1 item to select (dumb).
// as we remove the date just prior to the popup opening, here, we
// add it again, so it doesn't look like anything happened to the
// user
// when in fact we removed and added something in the blink of an
// eye, just so that the combo would not open its own popup. This
// seems to work
// without a hitch -- fix: June 21, 2008
if (OS_CARBON && isReadOnly && mCarbonPrePopupDate != null) setDate(mCarbonPrePopupDate);
// bug fix: Apr 18, 2008 - if we do various operations prior to
// actually fetching any newly entered text into the
// (non-read only) combo, we'll lose that edit, so fetch the combo
// text now so that we can check it later down in the code
// reported by: B. Haje
parseTextDate(true);
String comboText = isFlat ? mFlatCombo.getText() : mCombo.getText();
mComboControl.setCapture(true);
// some weird bug with opening, selecting, closing, then opening
// again, which blanks out the mCombo the first time around..
if (!isFlat) mCombo.select(0);
// kill any old
if (isCalendarVisible()) {
// bug fix part #2, apr 23. Repeated klicking open/close will
// get here, so we need to do the same bug fix as before but
// somewhat differently
// as we need to update the date object as well. This is
// basically only for non-read-only combos, but the fix is
// universally applicable.
setComboText(comboText);
parseTextDate(true);
kill(11);
return;
}
mComboControl.setFocus();
// bug fix: Apr 18, 2008 (see above)
// the (necessary) setFocus call above for some reason screws up any
// text entered by the user, so we actually have to set the text
// back onto the combo, despite the fact we pulled the data just a
// few lines ago.
setComboText(comboText);
getDisplay().addFilter(SWT.KeyDown, mKillListener);
getDisplay().addFilter(SWT.MouseDown, mOobClickListener);
mCalendarShell = new Shell(getDisplay().getActiveShell(), SWT.ON_TOP | SWT.NO_TRIM | SWT.NO_FOCUS);
mCalendarShell.setLayout(new FillLayout());
if (OS_CARBON) mCalendarShell.setSize(mSettings.getCalendarWidthMacintosh(), mSettings.getCalendarHeightMacintosh());
else mCalendarShell.setSize(mSettings.getCalendarWidth(), mSettings.getCalendarHeight());
mCalendarShell.addShellListener(new ShellListener() {
public void shellActivated(ShellEvent event) {
}
public void shellClosed(ShellEvent event) {
// shell killed
kill(12);
}
public void shellDeactivated(ShellEvent event) {
}
public void shellDeiconified(ShellEvent event) {
}
public void shellIconified(ShellEvent event) {
// shell no longer in focus (like.. WindowsKey + M on WinXP
// or such)
kill(13);
}
});
// if no date has been set, and we have another calendar widget to
// pull from, grab that date.
// if we do have a date set, load it as an object we can actually
// use.
Calendar pre = null;
if (comboText != null && comboText.length() > 1) {
try {
Date dat = DateHelper.getDate(comboText, mSettings.getDateFormat());
if (dat != null) {
pre = Calendar.getInstance(mSettings.getLocale());
pre.setTime(dat);
}
} catch (Exception err) {
List otherFormats = mSettings.getAdditionalDateFormats();
if (otherFormats != null) {
boolean dateSet = false;
try {
for (int i = 0; i < otherFormats.size(); i++) {
String format = (String) otherFormats.get(i);
Date date = DateHelper.getDate(comboText, format);
// success
setDate(date);
Calendar cal = Calendar.getInstance(mSettings.getLocale());
cal.setTime(date);
pre = cal;
dateSet = true;
break;
}
} catch (Exception err2) {
// don't care
// err2.printStackTrace();
}
if (!dateSet) {
// unparseable date, set the last used date if any,
// otherwise set nodateset text
if (mStartDate != null) setDate(mStartDate);
else {
setComboText(mSettings.getNoDateSetText());
}
}
} else {
// unparseable date, set the last used date if any,
// otherwise set nodateset text
if (mStartDate != null) setDate(mStartDate);
else {
setComboText(mSettings.getNoDateSetText());
}
}
}
} else {
if (mDependingCombo != null) {
// we need to pull the date from the depending combo's text
// area as it may be non-read-only, so we can't rely on the
// date
Calendar date = null;
try {
Date d = DateHelper.getDate(mDependingCombo.getCombo().getText(), mSettings.getDateFormat());
date = Calendar.getInstance(mSettings.getLocale());
date.setTime(d);
} catch (Exception err) {
date = mDependingCombo.getDate();
}
if (date != null) {
pre = Calendar.getInstance(mSettings.getLocale());
pre.setTime(date.getTime());
}
}
}
// create the calendar composite
mCalendarComposite = new CalendarComposite(mCalendarShell, pre, mDisallowBeforeDate, mDisallowAfterDate, mColorManager, mSettings, mAllowDateRange, mStartDate, mEndDate);
/* mCalendarComposite.addCalendarListener(new ICalendarListener() {
public void dateChanged(Calendar date) {
mStartDate = date;
notifyDateChanged();
}
public void dateRangeChanged(Calendar start, Calendar end) {
mStartDate = start;
mEndDate = end;
notifyDateChanged();
}
public void popupClosed() {
}
});
*/
mCalendarComposite.addMainCalendarListener(new ICalendarListener() {
public void dateChanged(Calendar date) {
if (!isFlat) mCombo.removeAll();
if (OS_CARBON) mCarbonPrePopupDate = date;
mStartDate = date;
if (date == null) {
setText("");
} else {
updateDate();
}
if (mStartDate == null) {
notifyDateChangedToNull();
} else {
notifyDateChanged();
}
}
public void dateRangeChanged(Calendar start, Calendar end) {
if (!isFlat) mCombo.removeAll();
mStartDate = start;
if (OS_CARBON) mCarbonPrePopupDate = start;
mEndDate = end;
if (start == null) {
setText("");
} else {
updateDate();
}
if (mDependingCombo != null) {
if (end != null) {
mDependingCombo.setDate(end);
} else {
mDependingCombo.setText("");
}
notifyDateRangeChanged();
} else {
notifyDateChanged();
}
}
public void popupClosed() {
kill(14);
for (int i = 0; i < mListeners.size(); i++) {
((ICalendarListener) mListeners.get(i)).popupClosed();
}
}
});
// figure out where to put the calendar composite shell
Point calLoc = mComboControl.getLocation();
Point size = mComboControl.getSize();
Point loc = null;
if (mSettings.showCalendarInRightCorner()) {
loc = new Point(calLoc.x + size.x - mCalendarShell.getSize().x, calLoc.y + size.y);
} else {
loc = new Point(calLoc.x, calLoc.y + size.y);
}
loc = toDisplay(loc);
// don't let it slip out on the left side of the screen
if (loc.x < 0) {
loc.x = 0;
}
mCalendarShell.setLocation(loc);
mCalendarShell.setVisible(true);
} catch (Exception e) {
mComboControl.setCapture(false);
e.printStackTrace();
// don't really care
}
}
/**
* Adds a {@link IDateParseExceptionListener} that listens to date parse exceptions
*
* @param listener to add
*/
public void addDateParseExceptionListener(IDateParseExceptionListener listener) {
checkWidget();
if (!mDateParseExceptionListeners.contains(listener)) {
mDateParseExceptionListeners.add(listener);
}
}
/**
* Removes a {@link IDateParseExceptionListener} listener.
*
* @param listener to remove
*/
public void removeDateParseExceptionListener(IDateParseExceptionListener listener) {
checkWidget();
mDateParseExceptionListeners.remove(listener);
}
/**
* Adds a calendar listener.
*
* @param listener Listener
*/
public void addCalendarListener(ICalendarListener listener) {
checkWidget();
if (!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
/**
* Removes a calendar listener.
*
* @param listener Listener
*/
public void removeCalendarListener(ICalendarListener listener) {
checkWidget();
mListeners.remove(listener);
}
/**
* Returns the combo box widget.
* <p>
* <font color="red"><b>NOTE:</b> The Combo box has a lot of listeners on it, please be "careful" when using it as
* you may cause unplanned-for things to happen.</font>
*
* @return Combo widget
*/
public Combo getCombo() {
checkWidget();
return mCombo;
}
public FlatCalendarCombo getCCombo() {
checkWidget();
return mFlatCombo;
}
private void updateDate() {
if (mStartDate == null) {
clear();
return;
}
String toSet = DateHelper.getDate(mStartDate, DateHelper.dateFormatFix(mSettings.getDateFormat()));
setText(toSet);
}
/**
* Removes date and clears combo, same as setDate(null).
*/
public void clear() {
if (isFlat) {
mFlatCombo.removeAll();
mFlatCombo.setText(mSettings.getNoDateSetText());
} else {
mCombo.removeAll();
mCombo.setText(mSettings.getNoDateSetText());
}
mStartDate = null;
mEndDate = null;
}
/**
* Puts focus on the combo box.
*
* @deprecated please use {@link #setFocus()}
*/
public void grabFocus() {
checkWidget();
if (isFlat) mFlatCombo.setFocus();
else mCombo.setFocus();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Composite#setFocus()
*/
public boolean setFocus() {
checkWidget();
if (this.isFlat) {
return this.getCCombo().getTextControl().setFocus();
} else {
return this.mCombo.setFocus();
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Control#forceFocus()
*/
public boolean forceFocus() {
checkWidget();
if (this.isFlat) {
return this.getCCombo().getTextControl().forceFocus();
} else {
return this.mCombo.forceFocus();
}
}
public void setEnabled(boolean enabled) {
checkWidget();
if (isFlat) mFlatCombo.setEnabled(enabled);
else mCombo.setEnabled(enabled);
}
public boolean isEnabled() {
checkWidget();
return isFlat ? mFlatCombo.getEnabled() : mCombo.getEnabled();
}
/**
* Adds a modification listener to the combo box.
*
* @param ml ModifyListener
*/
public void addModifyListener(ModifyListener ml) {
checkWidget();
if (isFlat) {
mFlatCombo.addModifyListener(ml);
} else {
mCombo.addModifyListener(ml);
}
}
/**
* Removes a modification listener from the combo box.
*
* @param ml ModifyListener
*/
public void removeModifyListener(ModifyListener ml) {
checkWidget();
if (isFlat) {
mFlatCombo.removeModifyListener(ml);
} else {
mCombo.removeModifyListener(ml);
}
}
/**
* The date prior to which selection is not allowed.
*
* @return Date
*/
public Calendar getDisallowBeforeDate() {
return mDisallowBeforeDate;
}
/**
* Sets the date prior to which selection is not allowed.
*
* @param disallowBeforeDate Date
*/
public void setDisallowBeforeDate(Calendar disallowBeforeDate) {
mDisallowBeforeDate = disallowBeforeDate;
}
/**
* Sets the date prior to which selection is not allowed.
*
* @param disallowBeforeDate Date
*/
public void setDisallowBeforeDate(Date disallowBeforeDate) {
Calendar cal = Calendar.getInstance(mSettings.getLocale());
cal.setTime(disallowBeforeDate);
mDisallowBeforeDate = cal;
}
/**
* The date after which selection is not allowed.
*
* @return Date
*/
public Calendar getDisallowAfterDate() {
return mDisallowAfterDate;
}
/**
* Sets the date after which selection is not allowed.
*
* @param disallowAfterDate Date
*/
public void setDisallowAfterDate(Calendar disallowAfterDate) {
mDisallowAfterDate = disallowAfterDate;
}
/**
* Sets the date after which selection is not allowed.
*
* @param disallowAfterDate
*/
public void setDisallowAfterDate(Date disallowAfterDate) {
Calendar cal = Calendar.getInstance(mSettings.getLocale());
cal.setTime(disallowAfterDate);
mDisallowAfterDate = cal;
}
/**
* Sets the CalendarCombo that will be the recipient of (end date) date range changes as well as date start date
* that will be used.
*
* @param combo CalendarCombo
*/
public void setDependingCombo(CalendarCombo combo) {
mDependingCombo = combo;
}
/**
* Returns the CalendarCombo that is the recipient of (end date) date range changes as well as date start date that
* will be used.
*
* @return CalendarCombo
*/
public CalendarCombo getDependingCombo() {
return mDependingCombo;
}
public void addControlListener(ControlListener listener) {
mComboControl.addControlListener(listener);
}
public void addDragDetectListener(DragDetectListener listener) {
mComboControl.addDragDetectListener(listener);
}
public void addFocusListener(FocusListener listener) {
mComboControl.addFocusListener(listener);
}
public void addHelpListener(HelpListener listener) {
mComboControl.addHelpListener(listener);
}
public void addKeyListener(KeyListener listener) {
mComboControl.addKeyListener(listener);
}
public void addMenuDetectListener(MenuDetectListener listener) {
mComboControl.addMenuDetectListener(listener);
}
public void addMouseListener(MouseListener listener) {
mComboControl.addMouseListener(listener);
}
public void addMouseMoveListener(MouseMoveListener listener) {
mComboControl.addMouseMoveListener(listener);
}
public void addMouseTrackListener(MouseTrackListener listener) {
mComboControl.addMouseTrackListener(listener);
}
public void addMouseWheelListener(MouseWheelListener listener) {
mComboControl.addMouseWheelListener(listener);
}
public void addPaintListener(PaintListener listener) {
mComboControl.addPaintListener(listener);
}
public void addTraverseListener(TraverseListener listener) {
mComboControl.addTraverseListener(listener);
}
public void addDisposeListener(DisposeListener listener) {
mComboControl.addDisposeListener(listener);
}
public void addListener(int eventType, Listener listener) {
mComboControl.addListener(eventType, listener);
}
public void removeControlListener(ControlListener listener) {
mComboControl.removeControlListener(listener);
}
public void removeDragDetectListener(DragDetectListener listener) {
mComboControl.removeDragDetectListener(listener);
}
public void removeFocusListener(FocusListener listener) {
mComboControl.removeFocusListener(listener);
}
public void removeHelpListener(HelpListener listener) {
mComboControl.removeHelpListener(listener);
}
public void removeKeyListener(KeyListener listener) {
mComboControl.removeKeyListener(listener);
}
public void removeMenuDetectListener(MenuDetectListener listener) {
mComboControl.removeMenuDetectListener(listener);
}
public void removeMouseListener(MouseListener listener) {
mComboControl.removeMouseListener(listener);
}
public void removeMouseMoveListener(MouseMoveListener listener) {
mComboControl.removeMouseMoveListener(listener);
}
public void removeMouseTrackListener(MouseTrackListener listener) {
mComboControl.removeMouseTrackListener(listener);
}
public void removeMouseWheelListener(MouseWheelListener listener) {
mComboControl.removeMouseWheelListener(listener);
}
public void removePaintListener(PaintListener listener) {
mComboControl.removePaintListener(listener);
}
public void removeTraverseListener(TraverseListener listener) {
mComboControl.removeTraverseListener(listener);
}
public void notifyListeners(int eventType, Event event) {
mComboControl.notifyListeners(eventType, event);
}
public void removeDisposeListener(DisposeListener listener) {
mComboControl.removeDisposeListener(listener);
}
public void removeListener(int eventType, Listener listener) {
mComboControl.removeListener(eventType, listener);
}
public Composite getActiveComboControl() {
return mComboControl;
}
}