/* 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(); } } } }