/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG and others.
* 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:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.ui.swt;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.util.Util;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DateTime;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.riena.ui.swt.facades.SWTFacade;
import org.eclipse.riena.ui.swt.layout.DpiGridLayoutFactory;
import org.eclipse.riena.ui.swt.lnf.LnfKeyConstants;
import org.eclipse.riena.ui.swt.lnf.LnfManager;
import org.eclipse.riena.ui.swt.utils.SwtUtilities;
/**
* Composite of a DatePicker and a SWT Text widget.
*
* @since 1.2
*/
public class DatePickerComposite extends Composite {
private final static int BUTTON_WIDTH = 16;
private final static int BUTTON_HEIGHT = 16;
private final Text textfield;
private final Button pickerButton;
private Color bgColor;
private DatePicker datePicker;
private IDateConverterStrategy dateConverterStrategy;
public DatePickerComposite(final Composite parent, final int textStyles) {
super(parent, SWT.BORDER);
DpiGridLayoutFactory.fillDefaults().numColumns(2).spacing(0, 0).applyTo(this);
textfield = new Text(this, checkStyle(textStyles));
setBackground(LnfManager.getLnf().getColor(LnfKeyConstants.SUB_MODULE_BACKGROUND));
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(textfield);
new EventForwarder(textfield, this);
pickerButton = new Button(this, SWT.ARROW | SWT.DOWN);
GridDataFactory.fillDefaults().grab(false, false).align(SWT.RIGHT, SWT.FILL).hint(BUTTON_WIDTH, BUTTON_HEIGHT).applyTo(pickerButton);
pickerButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
handleClick();
}
});
dateConverterStrategy = new RegexDateConverterStrategy(textfield);
}
@Override
public void dispose() {
if (datePicker != null) {
datePicker.dispose();
}
super.dispose();
}
public IDateConverterStrategy getDateConverterStrategy() {
return dateConverterStrategy;
}
public Text getTextfield() {
return textfield;
}
public void setDateConverterStrategy(final IDateConverterStrategy dateConverterStrategy) {
this.dateConverterStrategy = dateConverterStrategy;
}
/**
* @since 3.0
*/
@Override
public void setEnabled(final boolean enabled) {
super.setEnabled(enabled);
updateButtonEnablement();
updateBgColor(enabled);
}
@Override
public void setForeground(final Color color) {
super.setForeground(color);
textfield.setForeground(color);
}
@Override
public void setBackground(final Color color) {
this.bgColor = color;
updateBgColor(isEnabled());
}
@Override
public void setToolTipText(final String string) {
super.setToolTipText(string);
textfield.setToolTipText(string);
}
/**
* Updates the enabled state of the picker button, based on the composite's enabled state and the text fields editable state.
*
* @since 3.0
*/
public void updateButtonEnablement() {
if (!isDisposed()) {
final boolean enabledButton = getEnabled() && textfield.getEditable();
pickerButton.setEnabled(enabledButton);
}
}
// helping methods
//////////////////
/**
* Removes the {@link SWT.BORDER} style, to prevent a awkward representation
*
* @param style
* the SWT style bits
* @return the style bits without SWT.BORDER
*/
private int checkStyle(int style) {
if ((style & SWT.BORDER) != 0) {
style &= ~SWT.BORDER;
}
return style;
}
private void handleClick() {
if (!(textfield.isEnabled() && textfield.getEditable())) {
return;
}
if (datePicker == null || datePicker.isDisposed()) {
datePicker = new DatePicker(this);
}
if (!datePicker.isVisible()) {
final Point p = textfield.toDisplay(textfield.getLocation().x, textfield.getLocation().y);
datePicker.setLocation(p.x, p.y + textfield.getBounds().height);
datePicker.open(parseDate(textfield.getText()));
} else {
datePicker.close();
}
}
private Button getPickerButton() {
return pickerButton;
}
private Calendar parseDate(final String dateString) {
Calendar result = null;
final Date date = getDateConverterStrategy().getDateFromTextField(dateString);
if (date != null) {
result = Calendar.getInstance();
result.setTime(date);
}
return result;
}
private void updateBgColor(final boolean isEnabled) {
final Color color = isEnabled ? bgColor : getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
super.setBackground(color);
textfield.setBackground(color);
}
// helping classes
//////////////////
/**
* Strategy for converting between {@link String} and {@link Date}
*/
public static interface IDateConverterStrategy {
/**
* Parses the given date and sets it to the textfield
*
* @param date
*/
void setDateToTextField(Date date);
/**
* Converts the given dateString to a {@link Date}
*
* @param dateString
* @return
*/
Date getDateFromTextField(String dateString);
}
/**
* This class shows and hides a DateTime "date picker" on request. *
* <p>
* This class is NOT API - do not reference. Public for testing only.
*/
public static final class DatePicker {
private Shell shell;
private DateTime calendar;
private DatePickerComposite datePicker;
/**
* On windows the Calendar widget has a header that has a zoomOut / zoomIn ability. We need to keep count of the clicks needed until we can close the
* widget (i.e. last zoom in level). See Bug 288354, comment #4, point #3.
*/
private int clicksToClose = 1;
/**
* Create a new DatePicker instance.
* <p>
* You must invoke {@link #dispose()} to give up native resources held by this class.
*
* @param text
* a SWT text field that will received the picked date.
* @param pickerButton
* the button that will trigger showing the date picker.
*/
protected DatePicker(final DatePickerComposite datePicker) {
this.datePicker = datePicker;
shell = new Shell(datePicker.getShell(), SWT.NO_TRIM | SWT.ON_TOP);
shell.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_BLACK));
DpiGridLayoutFactory.fillDefaults().margins(1, 1).applyTo(shell);
calendar = new DateTime(shell, SWT.CALENDAR | SWT.SHORT);
calendar.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
shell.pack();
calendar.addMouseListener(new MouseAdapter() {
private final IZoneFinder zoneFinder = createZoneFinder(calendar);
@Override
public void mouseUp(final MouseEvent e) {
if (e.button != 1) {
return;
}
if (zoneFinder.getZone() == IZoneFinder.BODY) {
clicksToClose = Math.max(clicksToClose - 1, 0);
} else if (zoneFinder.getZone() == IZoneFinder.ZOOM_OUT) {
clicksToClose = Math.min(4, clicksToClose + 1);
}
if (clicksToClose == 0) {
setDateToTextfield();
clicksToClose = 1;
close();
}
}
});
calendar.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
if (e.character == SWT.CR) {
clicksToClose = Math.max(clicksToClose - 1, 0);
if (clicksToClose == 0) {
setDateToTextfield();
clicksToClose = 1;
close();
}
}
}
});
calendar.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(final FocusEvent e) {
final Display display = e.widget.getDisplay();
final Control focusControl = display.getCursorControl();
if (focusControl != datePicker.getPickerButton()) {
close();
}
}
});
}
public void dispose() {
if (!SwtUtilities.isDisposed(shell)) {
shell.dispose();
}
}
public void setLocation(final int x, final int y) {
shell.setLocation(x, y);
}
public boolean isDisposed() {
return shell == null || shell.isDisposed();
}
public boolean isVisible() {
return shell.isVisible();
}
public void close() {
shell.setVisible(false);
}
public void open(Calendar newDate) {
if (isVisible()) {
return;
}
shell.open();
shell.setVisible(true);
// if no date was supplied, use the current date
if (null == newDate) {
newDate = Calendar.getInstance();
newDate.setTime(new Date());
}
calendar.setYear(newDate.get(Calendar.YEAR));
calendar.setMonth(newDate.get(Calendar.MONTH));
calendar.setDay(newDate.get(Calendar.DAY_OF_MONTH));
}
// helping methods
//////////////////
private IZoneFinder createZoneFinder(final DateTime parent) {
IZoneFinder result;
if (isVistaOrLater()) {
result = new VistaZoneFinder(parent);
} else if (Util.isWin32()) {
result = new XPZoneFinder(parent);
} else if (Util.isMac()) {
result = new MacZoneFinder(parent);
} else {
result = new DefaultZoneFinder();
}
return result;
}
private boolean isVistaOrLater() {
boolean result = false;
if (Util.isWin32()) {
final String osVer = System.getProperty("os.version"); //$NON-NLS-1$
try {
final double version = Double.valueOf(osVer);
result = version >= 6.0;
} catch (final NumberFormatException nfe) {
// ignore
} catch (final NullPointerException npe) {
// ignore
}
}
return result;
}
private void setDateToTextfield() {
final Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, calendar.getYear());
cal.set(Calendar.MONTH, calendar.getMonth());
cal.set(Calendar.DAY_OF_MONTH, calendar.getDay());
datePicker.getDateConverterStrategy().setDateToTextField(cal.getTime());
}
}
/**
* Determines what will happen on a click, based on the current cursor position within the widget (header / body / zoom out area).
*/
private interface IZoneFinder extends MouseMoveListener {
/**
* The cursor is in the body of the native picker. Clicking here selects a date.
*/
int BODY = 0;
/**
* The cursor is in the header of the native picker. Clicking here does not select a date.
*/
int HEADER = 1;
/**
* The cursor is in the zoom out area of the native picker (vista / win7). Clicking here zoom's out. Adds an extra click (zoom in) to the number of
* click's required to close the picker (!).
*/
int ZOOM_OUT = 2;
int getZone();
}
/**
* {@inheritDoc}
* <p>
* Calculations for Vista and Win7.
*/
private static final class VistaZoneFinder implements IZoneFinder {
/** Height of the header. */
final int headerHeight = 48;
/** Height of 'dead' zone at top of header. */
final int topDeadZone = 4;
/** Height of 'dead' zone at bottom of header. */
final int bottomDeadZone = 16;
/** Top of the buttons. */
final int buttonTop = 9;
/** Bottom of the buttons. */
final int buttonBottom = 26;
/** Right end of the button / Left start of header. */
final int headerLeft = 20;
/** right end of header / Left start of button. */
final int headerRight = 204;
private int zone = BODY;
public VistaZoneFinder(final DateTime parent) {
SWTFacade.getDefault().addMouseMoveListener(parent, this);
}
public void mouseMove(final MouseEvent e) {
if (e.y < headerHeight) {
if (e.y > headerHeight - bottomDeadZone || e.y < topDeadZone) {
zone = HEADER;
} else {
if ((e.x < headerLeft && e.y > buttonTop && e.y < buttonBottom) || (e.x > headerRight && e.y > buttonTop && e.y < buttonBottom)) {
zone = HEADER;
} else {
zone = ZOOM_OUT;
}
}
} else {
zone = BODY;
}
}
public int getZone() {
return zone;
}
}
/**
* {@inheritDoc}
* <p>
* Calculations for XP (or earlier).
*/
private static final class XPZoneFinder implements IZoneFinder {
/** Height of the header. */
final int headerHeight = 45;
private int zone = BODY;
public XPZoneFinder(final DateTime parent) {
SWTFacade.getDefault().addMouseMoveListener(parent, this);
}
public void mouseMove(final MouseEvent e) {
zone = e.y < headerHeight ? HEADER : BODY;
}
public int getZone() {
return zone;
}
}
/**
* {@inheritDoc}
* <p>
* Calculations for Mac.
*/
private static final class MacZoneFinder implements IZoneFinder {
/** Height of the header. */
final int headerHeight = 40;
private int zone = BODY;
public MacZoneFinder(final DateTime parent) {
SWTFacade.getDefault().addMouseMoveListener(parent, this);
}
public void mouseMove(final MouseEvent e) {
zone = e.y < headerHeight ? HEADER : BODY;
}
public int getZone() {
return zone;
}
}
/**
* {@inheritDoc}
* <p>
* Generic implementation. Always assumes to be in the 'body' zone.
*/
private static final class DefaultZoneFinder implements IZoneFinder {
public void mouseMove(final MouseEvent e) {
// unused
}
public int getZone() {
return BODY;
}
}
/**
* Default implementation for a {@link IDateConverterStrategy} that will be used, when no other implementation was supplied. This implementation tries to
* parse the Date with a Regular Expression, but does only support simple DateFormats like "dd.MM.yyyy" and "dd.MM.yyyy HH:mm"
*/
private static class RegexDateConverterStrategy implements IDateConverterStrategy {
private final Text textfield;
public RegexDateConverterStrategy(final Text textfield) {
this.textfield = textfield;
}
public void setDateToTextField(final Date date) {
final String dateString = new SimpleDateFormat("dd.MM.yyyy").format(date); //$NON-NLS-1$
final String oldText = textfield.getText();
if (oldText.contains(":")) { //$NON-NLS-1$
final Pattern pattern = Pattern.compile("([ \\d\\.]+)\\s+(.*?:.*?)"); //$NON-NLS-1$
final Matcher matcher = pattern.matcher(oldText);
if (matcher.matches()) {
final String oldTime = matcher.group(2);
textfield.setText(dateString + " " + oldTime); //$NON-NLS-1$
}
} else {
textfield.setText(dateString);
}
textfield.setFocus();
}
public Date getDateFromTextField(final String dateString) {
Calendar result = null;
final Pattern pattern = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+).*?"); //$NON-NLS-1$
final Matcher matcher = pattern.matcher(dateString);
if (matcher.matches() && matcher.groupCount() == 3) {
int month = Integer.parseInt(matcher.group(2));
month -= 1;
int year = Integer.parseInt(matcher.group(3));
if (year < 100) {
year += 1900;
}
result = Calendar.getInstance();
result.set(Calendar.DAY_OF_MONTH, Integer.parseInt(matcher.group(1)));
result.set(Calendar.MONTH, month);
result.set(Calendar.YEAR, year);
return result.getTime();
}
return null;
}
}
}