/*
Copyright 2013 BarD Software s.r.o
This file is part of GanttProject, an opensource project management tool.
GanttProject is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
GanttProject is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GanttProject. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sourceforge.ganttproject.calendar;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Rectangle;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.swing.AbstractCellEditor;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import net.sourceforge.ganttproject.GPLogger;
import net.sourceforge.ganttproject.gui.AbstractTableAndActionsComponent;
import net.sourceforge.ganttproject.gui.UIFacade;
import net.sourceforge.ganttproject.gui.UIUtil;
import net.sourceforge.ganttproject.gui.UIUtil.GPDateCellEditor;
import net.sourceforge.ganttproject.gui.options.OptionsPageBuilder;
import net.sourceforge.ganttproject.gui.options.OptionsPageBuilder.ValueValidator;
import net.sourceforge.ganttproject.gui.taskproperties.CommonPanel;
import net.sourceforge.ganttproject.language.GanttLanguage;
import net.sourceforge.ganttproject.util.collect.Pair;
import biz.ganttproject.core.calendar.CalendarEvent;
import biz.ganttproject.core.calendar.CalendarEvent.Type;
import biz.ganttproject.core.calendar.GPCalendar;
import biz.ganttproject.core.option.DefaultColorOption;
import biz.ganttproject.core.option.ValidationException;
import biz.ganttproject.core.time.CalendarFactory;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
/**
* Implements a calendar editor component which consists of a table with calendar events (three columns: date, title, type)
* and Add/Delete buttons
*
* @author dbarashev (Dmitry Barashev)
*/
public class CalendarEditorPanel {
private static String getI18NedEventType(CalendarEvent.Type type) {
return GanttLanguage.getInstance().getText(
"calendar.editor.column." + TableModelImpl.Column.TYPE.name().toLowerCase() + ".value." + type.name().toLowerCase());
}
private static List<String> TYPE_COLUMN_VALUES = Lists.transform(Arrays.asList(CalendarEvent.Type.values()), new Function<CalendarEvent.Type, String>() {
@Override
public String apply(Type eventType) {
return getI18NedEventType(eventType);
}
});
private static final Runnable NOOP_CALLBACK = new Runnable() {
@Override public void run() {
}
};
private final List<CalendarEvent> myOneOffEvents = Lists.newArrayList();
private final List<CalendarEvent> myRecurringEvents = Lists.newArrayList();
private final TableModelImpl myRecurringModel;
private final TableModelImpl myOneOffModel;
private final Runnable myOnChangeCallback;
private final Runnable myOnCreate;
private final UIFacade myUiFacade;
private static Predicate<CalendarEvent> recurring(final boolean isRecurring) {
return new Predicate<CalendarEvent>() {
@Override
public boolean apply(CalendarEvent event) {
return event.isRecurring == isRecurring;
}
};
}
public CalendarEditorPanel(UIFacade uifacade, List<CalendarEvent> events, Runnable onChange) {
myOneOffEvents.addAll(Collections2.filter(events, recurring(false)));
myRecurringEvents.addAll(Collections2.filter(events, recurring(true)));
myOnChangeCallback = onChange == null ? NOOP_CALLBACK : onChange;
myOnCreate = NOOP_CALLBACK;
myUiFacade = uifacade;
myRecurringModel = new TableModelImpl(myRecurringEvents, myOnChangeCallback, true);
myOneOffModel = new TableModelImpl(myOneOffEvents, myOnChangeCallback, false);
}
public CalendarEditorPanel(UIFacade uifacade, final GPCalendar calendar, Runnable onChange) {
myUiFacade = uifacade;
myOnChangeCallback = onChange == null ? NOOP_CALLBACK : onChange;
myOnCreate = new Runnable() {
@Override
public void run() {
reload(calendar);
}
};
myRecurringModel = new TableModelImpl(myRecurringEvents, myOnChangeCallback, true);
myOneOffModel = new TableModelImpl(myOneOffEvents, myOnChangeCallback, false);
}
public void reload(GPCalendar calendar) {
reload(calendar, myOneOffEvents, myOneOffModel);
reload(calendar, myRecurringEvents, myRecurringModel);
}
private static void reload(GPCalendar calendar, List<CalendarEvent> events, TableModelImpl model) {
int size = events.size();
events.clear();
model.fireTableRowsDeleted(0, size);
events.addAll(Collections2.filter(calendar.getPublicHolidays(), recurring(model.isRecurring())));
model.fireTableRowsInserted(0, events.size());
}
public JComponent createComponent() {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab(GanttLanguage.getInstance().getText("calendar.editor.tab.oneoff.title"),
createNonRecurringComponent());
tabbedPane.addTab(GanttLanguage.getInstance().getText("calendar.editor.tab.recurring.title"),
createRecurringComponent());
myOnCreate.run();
return tabbedPane;
}
private Component createRecurringComponent() {
DateFormat dateFormat = GanttLanguage.getInstance().getRecurringDateFormat();
AbstractTableAndActionsComponent<CalendarEvent> tableAndActions = createTableComponent(myRecurringModel, dateFormat, myUiFacade);
JPanel result = AbstractTableAndActionsComponent.createDefaultTableAndActions(tableAndActions.getTable(), tableAndActions.getActionsComponent());
Date today = CalendarFactory.newCalendar().getTime();
final String hint = GanttLanguage.getInstance().formatText("calendar.editor.dateHint", dateFormat.format(today));
Pair<JLabel,? extends TableCellEditor> validator = createDateValidatorComponents(hint, dateFormat);
TableColumn dateColumn = tableAndActions.getTable().getColumnModel().getColumn(TableModelImpl.Column.DATES.ordinal());
dateColumn.setCellEditor(validator.second());
result.add(validator.first(), BorderLayout.SOUTH);
result.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
return result;
}
public JPanel createNonRecurringComponent() {
AbstractTableAndActionsComponent<CalendarEvent> tableAndActions = createTableComponent(myOneOffModel, GanttLanguage.getInstance().getShortDateFormat(), myUiFacade);
JPanel result = AbstractTableAndActionsComponent.createDefaultTableAndActions(tableAndActions.getTable(), tableAndActions.getActionsComponent());
Date today = CalendarFactory.newCalendar().getTime();
final String hint = GanttLanguage.getInstance().formatText("calendar.editor.dateHint",
GanttLanguage.getInstance().getMediumDateFormat().format(today), GanttLanguage.getInstance().getShortDateFormat().format(today));
Pair<JLabel,? extends TableCellEditor> validator = createDateValidatorComponents(hint, GanttLanguage.getInstance().getMediumDateFormat(), GanttLanguage.getInstance().getShortDateFormat());
TableColumn dateColumn = tableAndActions.getTable().getColumnModel().getColumn(TableModelImpl.Column.DATES.ordinal());
dateColumn.setCellEditor(validator.second());
result.add(validator.first(), BorderLayout.SOUTH);
result.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
return result;
}
private static Pair<JLabel, ? extends TableCellEditor> createDateValidatorComponents(final String hint, DateFormat... dateFormats) {
final JLabel hintLabel = new JLabel(" "); // non-empty label to occupy some vertical space
final ValueValidator<Date> realValidator = UIUtil.createStringDateValidator(null, dateFormats);
ValueValidator<Date> decorator = new ValueValidator<Date>() {
@Override
public Date parse(String text) throws ValidationException {
try {
Date result = realValidator.parse(text);
hintLabel.setText("");
return result;
} catch (ValidationException e) {
e.printStackTrace();
hintLabel.setText(hint);
throw e;
}
}
};
GPDateCellEditor dateEditor = new GPDateCellEditor(null, true, decorator, dateFormats);
return Pair.create(hintLabel, dateEditor);
}
static interface FocusSetter {
void setFocus(int row);
}
static class ColorEditor extends AbstractCellEditor implements TableCellEditor {
private final OptionsPageBuilder.ColorComponent myEditor;
private final DefaultColorOption myOption;
private final FocusSetter myFocusMover;
ColorEditor(UIFacade uiFacade, FocusSetter focusSetter) {
myOption = new DefaultColorOption("sadf") {
@Override
protected void resetValue(Color value, boolean resetInitial, Object clientId) {
super.resetValue(value, resetInitial, clientId);
if (clientId != this) {
stopCellEditing();
}
}
};
OptionsPageBuilder builder = new OptionsPageBuilder();
builder.setUiFacade(uiFacade);
myEditor = builder.createColorComponent(myOption);
myFocusMover = focusSetter;
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, final int row, int column) {
TableCellRenderer renderer = table.getCellRenderer(row, column);
Component c = renderer.getTableCellRendererComponent(table, value, isSelected, true, row, column);
if (c != null) {
myEditor.getJComponent().setOpaque(true);
myEditor.getJComponent().setBackground(c.getBackground());
if (c instanceof JComponent) {
myEditor.getJComponent().setBorder(((JComponent) c).getBorder());
}
} else {
myEditor.getJComponent().setOpaque(false);
}
myOption.setValue((Color)value, this);
final FocusListener onStartEditing = new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
myEditor.getJComponent().removeFocusListener(this);
myEditor.openChooser();
}
};
myEditor.getJComponent().addFocusListener(onStartEditing);
myEditor.setOnCancelCallback(new Runnable() {
@Override
public void run() {
cancelCellEditing();
myEditor.getJComponent().removeFocusListener(onStartEditing);
moveFocusToTable(row);
}
});
myEditor.setOnOkCallback(new Runnable() {
@Override
public void run() {
myEditor.getJComponent().removeFocusListener(onStartEditing);
moveFocusToTable(row);
}
});
return myEditor.getJComponent();
}
private void moveFocusToTable(final int row) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
myFocusMover.setFocus(row);
}
});
}
@Override
public Object getCellEditorValue() {
return myOption.getValue();
}
}
static class DateCellRendererImpl implements TableCellRenderer {
private final DefaultTableCellRenderer myDefaultRenderer = new DefaultTableCellRenderer();
private final DateFormat myDateFormat;
DateCellRendererImpl(DateFormat dateFormat) {
myDateFormat = dateFormat;
}
@Override
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
assert (value == null || value instanceof CalendarEvent) : (value == null)
? "value is null" : String.format("value=%s class=%s", value, value.getClass());
final String formattedDate;
if (value == null) {
formattedDate = "";
} else {
CalendarEvent e = (CalendarEvent) value;
formattedDate = myDateFormat.format(e.myDate);
}
JLabel result = (JLabel) myDefaultRenderer.getTableCellRendererComponent(table, formattedDate, isSelected, hasFocus,
row, column);
return result;
}
}
private static AbstractTableAndActionsComponent<CalendarEvent> createTableComponent(final TableModelImpl tableModel, final DateFormat dateFormat, UIFacade uiFacade) {
final JTable table = new JTable(tableModel);
UIUtil.setupTableUI(table);
CommonPanel.setupComboBoxEditor(
table.getColumnModel().getColumn(TableModelImpl.Column.TYPE.ordinal()),
TYPE_COLUMN_VALUES.toArray(new String[0]));
//myTable.getColumnModel().getColumn(TableModelImpl.Column.RECURRING.ordinal()).setCellRenderer(myTable.getDefaultRenderer(TableModelImpl.Column.RECURRING.getColumnClass()));
// We'll show a hint label under the table if user types something which we can't parse
TableColumn dateColumn = table.getColumnModel().getColumn(TableModelImpl.Column.DATES.ordinal());
dateColumn.setCellRenderer(new DateCellRendererImpl(dateFormat));
TableColumn colorColumn = table.getColumnModel().getColumn(TableModelImpl.Column.COLOR.ordinal());
colorColumn.setCellRenderer(UIUtil.newColorRenderer(new Supplier<Integer>() {
@Override
public Integer get() {
return tableModel.getRowCount() - 1;
}
}));
colorColumn.setCellEditor(new ColorEditor(uiFacade, new FocusSetter() {
@Override
public void setFocus(int row) {
table.requestFocus();
table.getSelectionModel().setSelectionInterval(row, row);
}
}));
AbstractTableAndActionsComponent<CalendarEvent> tableAndActions = new AbstractTableAndActionsComponent<CalendarEvent>(table) {
@Override
protected void onAddEvent() {
int lastRow = tableModel.getRowCount() - 1;
Rectangle cellRect = table.getCellRect(lastRow, 0, true);
table.scrollRectToVisible(cellRect);
table.getSelectionModel().setSelectionInterval(lastRow, lastRow);
table.editCellAt(lastRow, 0);
table.getEditorComponent().requestFocus();
}
@Override
protected void onDeleteEvent() {
if (table.getSelectedRow() < tableModel.getRowCount() - 1) {
tableModel.delete(table.getSelectedRow());
}
}
@Override
protected CalendarEvent getValue(int row) {
return tableModel.getValue(row);
}
};
Function<List<CalendarEvent>, Boolean> isDeleteEnabled = new Function<List<CalendarEvent>, Boolean>() {
@Override
public Boolean apply(List<CalendarEvent> events) {
if (events.size() == 1 && events.get(0) == null) {
return false;
}
return true;
}
};
tableAndActions.getDeleteItemAction().putValue(AbstractTableAndActionsComponent.PROPERTY_IS_ENABLED_FUNCTION, isDeleteEnabled);
return tableAndActions;
}
public List<CalendarEvent> getEvents() {
List<CalendarEvent> result = Lists.newArrayList();
result.addAll(myOneOffEvents);
result.addAll(myRecurringEvents);
return result;
}
private static class TableModelImpl extends AbstractTableModel {
private static enum Column {
DATES(CalendarEvent.class, null), SUMMARY(String.class, ""), TYPE(String.class, ""), COLOR(Color.class, Color.GRAY);
private String myTitle;
private Class<?> myClazz;
private Object myDefault;
Column(Class<?> clazz, Object defaultValue) {
myTitle = GanttLanguage.getInstance().getText("calendar.editor.column." + name().toLowerCase() + ".title");
myClazz = clazz;
myDefault = defaultValue;
}
public String getTitle() {
return myTitle;
}
public Class<?> getColumnClass() {
return myClazz;
}
public Object getDefault() {
return myDefault;
}
}
private final List<CalendarEvent> myEvents;
private final Runnable myOnChangeCallback;
private final boolean isRecurring;
TableModelImpl(List<CalendarEvent> events, Runnable onChangeCallback, boolean recurring) {
myEvents = events;
myOnChangeCallback = onChangeCallback;
isRecurring = recurring;
}
boolean isRecurring() {
return isRecurring;
}
CalendarEvent getValue(int row) {
return row < myEvents.size() ? myEvents.get(row) : null;
}
void delete(int row) {
myEvents.remove(row);
fireTableRowsDeleted(row, row);
myOnChangeCallback.run();
}
@Override
public int getColumnCount() {
return Column.values().length;
}
@Override
public int getRowCount() {
return myEvents.size() + 1;
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return Column.values()[columnIndex].getColumnClass();
}
@Override
public String getColumnName(int column) {
return Column.values()[column].getTitle();
}
@Override
public Object getValueAt(int row, int col) {
if (row < 0 || row >= getRowCount()) {
return null;
}
if (row == getRowCount() - 1) {
return Column.values()[col].getDefault();
}
CalendarEvent e = myEvents.get(row);
switch (Column.values()[col]) {
case DATES:
return e;
case SUMMARY:
return Objects.firstNonNull(e.getTitle(), "");
case TYPE:
return getI18NedEventType(e.getType());
case COLOR:
return Objects.firstNonNull(e.getColor(), Column.values()[col].getDefault());
}
return null;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return rowIndex < myEvents.size() || columnIndex == Column.DATES.ordinal();
}
@Override
public void setValueAt(Object aValue, int row, int col) {
if (row < 0 || row >= getRowCount()) {
return;
}
if (aValue == null) {
return;
}
String value = String.valueOf(aValue);
if (row == getRowCount() - 1) {
myEvents.add(CalendarEvent.newEvent(null, isRecurring, CalendarEvent.Type.HOLIDAY, "", null));
}
CalendarEvent e = myEvents.get(row);
CalendarEvent newEvent = null;
switch (Column.values()[col]) {
case DATES:
try {
Date date = GanttLanguage.getInstance().getShortDateFormat().parse(value);
newEvent = CalendarEvent.newEvent(date, e.isRecurring, e.getType(), e.getTitle(), e.getColor());
} catch (ParseException e1) {
GPLogger.log(e1);
}
break;
case SUMMARY:
newEvent = CalendarEvent.newEvent(e.myDate, e.isRecurring, e.getType(), value, e.getColor());
break;
case TYPE:
for (CalendarEvent.Type eventType : CalendarEvent.Type.values()) {
if (getI18NedEventType(eventType).equals(value)) {
newEvent = CalendarEvent.newEvent(e.myDate, e.isRecurring, eventType, e.getTitle(), e.getColor());
}
}
break;
case COLOR:
assert aValue instanceof Color : "Bug: we expect Color but we get " + aValue.getClass();
newEvent = CalendarEvent.newEvent(e.myDate, e.isRecurring, e.getType(), e.getTitle(), (Color)aValue);
break;
}
if (newEvent != null) {
myEvents.set(row, newEvent);
fireTableRowsUpdated(row, row + 1);
myOnChangeCallback.run();
}
}
}
}