jfxtras.internal.scene.control.skin.CalendarPickerControlSkin Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jfxtras-controls Show documentation
Show all versions of jfxtras-controls Show documentation
Miscellaneous components for JavaFX
The newest version!
/**
* Copyright (c) 2011-2021, JFXtras
* 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 organization 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 JFXTRAS 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 jfxtras.internal.scene.control.skin;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.converter.EnumConverter;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
import jfxtras.css.CssMetaDataForSkinProperty;
import jfxtras.css.converters.SimpleDateFormatConverter;
import jfxtras.scene.control.CalendarPicker;
import jfxtras.scene.control.CalendarTimePicker;
import jfxtras.scene.control.ListSpinner;
import jfxtras.scene.control.ListSpinnerIntegerList;
import jfxtras.scene.layout.GridPane;
import jfxtras.util.NodeUtil;
/**
* This skin uses regular JavaFX controls
* @author Tom Eugelink
*
*/
public class CalendarPickerControlSkin extends CalendarPickerMonthlySkinAbstract
{
// ==================================================================================================================
// CONSTRUCTOR
/**
*
*/
public CalendarPickerControlSkin(CalendarPicker control)
{
super(control);
construct();
}
/*
* construct the component
*/
private void construct()
{
// setup component
createNodes();
layoutNodes();
// start listening to changes
// if the calendar changes, the display calendar will jump to show that
getSkinnable().calendarProperty().addListener( (InvalidationListener) observable -> {
Calendar calendar = getSkinnable().getCalendar();
Calendar displayedCalendar = getSkinnable().getDisplayedCalendar();
if (calendar != null && (displayedCalendar == null || calendar.get(Calendar.YEAR) != displayedCalendar.get(Calendar.YEAR) || calendar.get(Calendar.MONTH) != displayedCalendar.get(Calendar.MONTH)) ) {
getSkinnable().setDisplayedCalendar(calendar);
}
});
if (getSkinnable().getCalendar() != null) {
getSkinnable().setDisplayedCalendar(getSkinnable().getCalendar());
}
// if the calendars change, the selection must be refreshed
getSkinnable().calendars().addListener( (InvalidationListener) observable -> {
refreshDayButtonToggleState();
});
// react to changes in the locale
getSkinnable().localeProperty().addListener( (InvalidationListener) observable -> {
monthListSpinner.setItems(FXCollections.observableArrayList(getMonthLabels()));
// force change the locale in the displayed calendar
getSkinnable().displayedCalendar().set( (Calendar)getSkinnable().getDisplayedCalendar().clone() );
refresh();
});
// react to changes in the locale
getSkinnable().showTimeProperty().addListener( (InvalidationListener) observable -> {
layoutNodes();
});
// react to changes in the disabled calendars
getSkinnable().disabledCalendars().addListener( new ListChangeListener(){
@Override
public void onChanged(javafx.collections.ListChangeListener.Change extends Calendar> change) {
refreshDayButtonsVisibilityAndLabel();
}
});
// react to changes in the highlighted calendars
getSkinnable().highlightedCalendars().addListener(new ListChangeListener(){
@Override
public void onChanged(javafx.collections.ListChangeListener.Change extends Calendar> arg0) {
refreshDayButtonsVisibilityAndLabel();
}
});
// react to changes in the displayed calendar
getSkinnable().displayedCalendar().addListener( (InvalidationListener) observable -> {
refresh();
});
// update the data
refresh();
}
// ==================================================================================================================
// PROPERTIES
/**
* This skin has the displayed date always pointing to the first of the month
* @param displayedCalendar
* @return
*/
private Calendar deriveDisplayedCalendar(Calendar displayedCalendar)
{
// always the 1st of the month, without time
Calendar lCalendar = Calendar.getInstance(getSkinnable().getLocale());
lCalendar.set(Calendar.DATE, 1);
lCalendar.set(Calendar.MONTH, displayedCalendar.get(Calendar.MONTH));
lCalendar.set(Calendar.YEAR, displayedCalendar.get(Calendar.YEAR));
lCalendar.set(Calendar.HOUR_OF_DAY, 0);
lCalendar.set(Calendar.MINUTE, 0);
lCalendar.set(Calendar.SECOND, 0);
lCalendar.set(Calendar.MILLISECOND, 0);
return lCalendar;
}
// ==================================================================================================================
// StyleableProperties
/** ShowWeeknumbers: */
public final ObjectProperty showWeeknumbersProperty() { return showWeeknumbers; }
private ObjectProperty showWeeknumbers = new SimpleStyleableObjectProperty(StyleableProperties.SHOW_WEEKNUMBERS, this, "showWeeknumbers", StyleableProperties.SHOW_WEEKNUMBERS.getInitialValue(null)) {
{ // anonymous constructor
addListener( (invalidationEvent) -> {
layoutNodes();
});
}
};
public final void setShowWeeknumbers(ShowWeeknumbers value) { showWeeknumbersProperty().set(value); }
public final ShowWeeknumbers getShowWeeknumbers() { return showWeeknumbers.get(); }
public final CalendarPickerControlSkin withShowWeeknumbers(ShowWeeknumbers value) { setShowWeeknumbers(value); return this; }
public enum ShowWeeknumbers {YES, NO}
/** LabelDateFormat: */
public final ObjectProperty labelDateFormatProperty() { return labelDateFormat; }
private ObjectProperty labelDateFormat = new SimpleStyleableObjectProperty(StyleableProperties.LABEL_DATEFORMAT, this, "labelDateFormat", StyleableProperties.LABEL_DATEFORMAT.getInitialValue(null)) {
{ // anonymous constructor
addListener( (invalidationEvent) -> {
refreshDayButtonsVisibilityAndLabel();
});
}
};
public final void setLabelDateFormat(DateFormat value) { labelDateFormatProperty().set(value); }
public final DateFormat getLabelDateFormat() { return labelDateFormat.get(); }
public final CalendarPickerControlSkin withLabelDateFormat(DateFormat value) { setLabelDateFormat(value); return this; }
static private final SimpleDateFormat ID_DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd");
// ----------------------------
// communicate the styleables
private static class StyleableProperties {
private static final CssMetaData SHOW_WEEKNUMBERS = new CssMetaDataForSkinProperty("-fxx-show-weeknumbers", new EnumConverter(ShowWeeknumbers.class), ShowWeeknumbers.YES ) {
@Override
protected ObjectProperty getProperty(CalendarPickerControlSkin s) {
return s.showWeeknumbersProperty();
}
};
private static final CssMetaData LABEL_DATEFORMAT = new CssMetaDataForSkinProperty("-fxx-label-dateformat", new SimpleDateFormatConverter(), new SimpleDateFormat("d") ) {
@Override
protected ObjectProperty getProperty(CalendarPickerControlSkin s) {
return s.labelDateFormatProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables = new ArrayList>(SkinBase.getClassCssMetaData());
styleables.add(SHOW_WEEKNUMBERS);
styleables.add(LABEL_DATEFORMAT);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* This method should delegate to {@link javafx.scene.Node#getClassCssMetaData()} so that
* a Node's CssMetaData can be accessed without the need for reflection.
* @return The CssMetaData associated with this node, which may include the
* CssMetaData of its super classes.
*/
public List> getCssMetaData() {
return getClassCssMetaData();
}
// ==================================================================================================================
// DRAW
/**
* construct the nodes
*/
private void createNodes()
{
// setup the grid so all weekday togglebuttons will grow, but the weeknumbers do not
ColumnConstraints lColumnConstraintsAlwaysGrow = new ColumnConstraints();
lColumnConstraintsAlwaysGrow.setHgrow(Priority.ALWAYS);
ColumnConstraints lColumnConstraintsNeverGrow = new ColumnConstraints();
lColumnConstraintsNeverGrow.setHgrow(Priority.NEVER);
// month spinner
List lMonthLabels = getMonthLabels();
monthListSpinner = new ListSpinner(lMonthLabels).withIndex(Calendar.getInstance().get(Calendar.MONTH)).withCyclic(Boolean.TRUE);
monthListSpinner.setId("monthListSpinner");
// on cycle overflow to year
monthListSpinner.setOnCycle( (cycleEvent) -> {
// if we've cycled down
if (cycleEvent.cycledDown())
{
yearListSpinner.increment();
}
else
{
yearListSpinner.decrement();
}
});
// if the value changed, update the displayed calendar
monthListSpinner.valueProperty().addListener( (ObservableValue extends String> observable, String oldValue, String newValue) -> {
setDisplayedCalendarFromSpinners();
});
// year spinner
yearListSpinner = new ListSpinner(new ListSpinnerIntegerList()).withValue(Calendar.getInstance().get(Calendar.YEAR));
yearListSpinner.setId("yearListSpinner");
// if the value changed, update the displayed calendar
yearListSpinner.valueProperty().addListener( (ObservableValue extends Integer> observable, Integer oldValue, Integer newValue) -> {
setDisplayedCalendarFromSpinners();
});
// double click here to show today
todayButton = new Button(" ");
todayButton.getStyleClass().add("today-button");
todayButton.setMinSize(16, 16);
todayButton.setOnAction((ActionEvent event) -> {
setToToday();
});
// weekday labels
for (int i = 0; i < 7; i++)
{
// create buttons
Label lLabel = new Label("" + i);
// style class is set together with the label
lLabel.getStyleClass().add("weekday-label");
lLabel.setMaxWidth(Integer.MAX_VALUE); // this is one of those times; why the @#$@#$@#$ do I need to specify this in order to make the damn label centered?
// remember the column it is associated with
lLabel.setUserData(Integer.valueOf(i));
lLabel.onMouseClickedProperty().set(weekdayLabelMouseClickedPropertyEventHandler);
// remember it
weekdayLabels.add(lLabel);
}
// weeknumber labels
for (int i = 0; i < 6; i++)
{
// create buttons
Label lLabel = new Label("" + i);
lLabel.getStyleClass().add("weeknumber");
lLabel.setAlignment(Pos.BASELINE_RIGHT);
// remember it
weeknumberLabels.add(lLabel);
// remember the row it is associated with
lLabel.setUserData(Integer.valueOf(i));
lLabel.onMouseClickedProperty().set(weeknumerLabelMouseClickedPropertyEventHandler);
}
// setup: 6 rows of 7 days per week (which is the maximum number of buttons required in the worst case layout)
for (int i = 0; i < 6 * 7; i++)
{
// create buttons
ToggleButton lToggleButton = new ToggleButton("" + i);
lToggleButton.setId("day" + i);
lToggleButton.getStyleClass().add("day-button");
lToggleButton.onMouseReleasedProperty().set(toggleButtonMouseReleasedPropertyEventHandler); // for minimal memory usage, use a single listener
lToggleButton.onKeyReleasedProperty().set(toggleButtonKeyReleasedPropertyEventHandler); // for minimal memory usage, use a single listener
// remember which button belongs to this property
booleanPropertyToDayToggleButtonMap.put(lToggleButton.selectedProperty(), lToggleButton);
// add it
lToggleButton.setMaxWidth(Double.MAX_VALUE); // make the button grow to fill a GridPane's cell
lToggleButton.setAlignment(Pos.BASELINE_CENTER);
// remember it
dayButtons.add(lToggleButton);
}
// add timepicker
Bindings.bindBidirectional(timePicker.calendarProperty(), getSkinnable().calendarProperty());
Bindings.bindBidirectional(timePicker.valueValidationCallbackProperty(), getSkinnable().valueValidationCallbackProperty());
Bindings.bindBidirectional(timePicker.localeProperty(), getSkinnable().localeProperty());
// add to self
getSkinnable().getStyleClass().add(this.getClass().getSimpleName()); // always add self as style class, because CSS should relate to the skin not the control
}
// the result
private ListSpinner monthListSpinner = null;
private ListSpinner yearListSpinner = null;
private Button todayButton = new Button(" ");
final private List