/* Copyright 2003-2012 Dmitry Barashev, GanttProject Team 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.gui; import biz.ganttproject.core.option.GPOption; import biz.ganttproject.core.option.ValidationException; import biz.ganttproject.core.time.CalendarFactory; import biz.ganttproject.core.time.GanttCalendar; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.Lists; import javafx.embed.swing.JFXPanel; import net.sourceforge.ganttproject.IGanttProject; import net.sourceforge.ganttproject.action.GPAction; import net.sourceforge.ganttproject.gui.options.OptionsPageBuilder; import net.sourceforge.ganttproject.gui.options.OptionsPageBuilder.ValueValidator; import net.sourceforge.ganttproject.language.GanttLanguage; import net.sourceforge.ganttproject.language.GanttLanguage.Event; import net.sourceforge.ganttproject.util.PropertiesUtil; import net.sourceforge.ganttproject.util.collect.Pair; import org.jdesktop.swingx.JXDatePicker; import org.jdesktop.swingx.JXTable; import org.jdesktop.swingx.decorator.ColorHighlighter; import org.jdesktop.swingx.decorator.ComponentAdapter; import org.jdesktop.swingx.decorator.HighlightPredicate; import org.jdesktop.swingx.decorator.Highlighter; import org.jdesktop.swingx.decorator.HighlighterFactory; import javax.swing.*; import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.HyperlinkListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.text.html.HTMLEditorKit; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.text.DateFormat; import java.text.ParseException; import java.util.Arrays; import java.util.Date; import java.util.Properties; public abstract class UIUtil { public static final Highlighter ZEBRA_HIGHLIGHTER = new ColorHighlighter(new HighlightPredicate() { @Override public boolean isHighlighted(Component renderer, ComponentAdapter adapter) { return adapter.row % 2 == 1; } }, new Color(0xf0, 0xf0, 0xe0), null); public static final Color ERROR_BACKGROUND = new Color(255, 191, 207); public static final Color INVALID_VALUE_BACKGROUND = new Color(255, 125, 125); public static final Color INVALID_FIELD_COLOR = Color.RED.brighter(); public static final Color PATINA_FOREGROUND = new Color(102, 153, 153); public static Font FONTAWESOME_FONT = null; private static Properties FONTAWESOME_PROPERTIES = new Properties(); static { ImageIcon calendarImage = new ImageIcon(UIUtil.class.getResource("/icons/calendar_16.gif")); ImageIcon nextMonth = new ImageIcon(UIUtil.class.getResource("/icons/nextmonth.gif")); ImageIcon prevMonth = new ImageIcon(UIUtil.class.getResource("/icons/prevmonth.gif")); UIManager.put("JXDatePicker.arrowDown.image", calendarImage); UIManager.put("JXMonthView.monthUp.image", prevMonth); UIManager.put("JXMonthView.monthDown.image", nextMonth); UIManager.put("JXMonthView.monthCurrent.image", calendarImage); try (InputStream is = UIUtil.class.getResourceAsStream("/fontawesome-webfont.ttf")) { Font font = Font.createFont(Font.TRUETYPE_FONT, is); FONTAWESOME_FONT = font.deriveFont(Font.PLAIN, 24f); } catch (IOException | FontFormatException e) { FONTAWESOME_FONT = null; } FONTAWESOME_PROPERTIES = new Properties(); PropertiesUtil.loadProperties(FONTAWESOME_PROPERTIES, "/fontawesome.properties"); } public static void setEnabledTree(JComponent root, final boolean isEnabled) { walkComponentTree(root, new Predicate<JComponent>() { @Override public boolean apply(JComponent input) { input.setEnabled(isEnabled); return true; } }); } public static void setBackgroundTree(JComponent root, final Color background) { walkComponentTree(root, new Predicate<JComponent>() { @Override public boolean apply(JComponent input) { input.setBackground(background); return true; } }); } public static void walkComponentTree(JComponent root, Predicate<JComponent> visitor) { if (visitor.apply(root)) { Component[] components = root.getComponents(); for (int i = 0; i < components.length; i++) { if (components[i] instanceof JComponent) { walkComponentTree((JComponent) components[i], visitor); } } } } public static void createTitle(JComponent component, String title) { Border lineBorder = BorderFactory.createMatteBorder(1, 0, 0, 0, Color.BLACK); component.setBorder(BorderFactory.createTitledBorder(lineBorder, title)); } public static void registerActions(JComponent component, boolean recursive, GPAction... actions) { for (GPAction action : actions) { for (KeyStroke ks : GPAction.getAllKeyStrokes(action.getID())) { pushAction(component, recursive, ks, action); } } } public static void pushAction(JComponent root, boolean recursive, KeyStroke keyStroke, GPAction action) { root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keyStroke, action.getID()); root.getActionMap().put(action.getID(), action); for (Component child : root.getComponents()) { if (child instanceof JComponent) { pushAction((JComponent) child, recursive, keyStroke, action); } } } public static void setupTableUI(JTable table, int visibleRows) { table.setPreferredScrollableViewportSize(new Dimension(table.getPreferredScrollableViewportSize().width, table.getRowHeight() * visibleRows)); Font font = table.getFont(); table.setRowHeight(table.getFontMetrics(font).getHeight() + 5); } public static void setupHighlighters(JXTable table) { table.setHighlighters(HighlighterFactory.createAlternateStriping(Color.WHITE, Color.ORANGE.brighter())); } public static void setupTableUI(JTable table) { setupTableUI(table, 10); } public static <T> DocumentListener attachValidator(final JTextField textField, final OptionsPageBuilder.ValueValidator<T> validator, final GPOption<T> option) { final DocumentListener listener = new DocumentListener() { private void saveValue() { try { T oldValue = option == null ? null : option.getValue(); T value = validator.parse(textField.getText()); if (option != null && !Objects.equal(oldValue, value)) { option.setValue(value, validator); } textField.setBackground(getValidFieldColor()); } /* If value in text filed is not integer change field color */ catch (NumberFormatException ex) { textField.setBackground(INVALID_FIELD_COLOR); } catch (ValidationException ex) { textField.setBackground(INVALID_FIELD_COLOR); } } @Override public void insertUpdate(DocumentEvent e) { saveValue(); } @Override public void removeUpdate(DocumentEvent e) { saveValue(); } @Override public void changedUpdate(DocumentEvent e) { saveValue(); } }; textField.getDocument().addDocumentListener(listener); return listener; } public static interface DateValidator extends Function<Date, Pair<Boolean, String>> { class Default { public static DateValidator aroundProjectStart(final Date projectStart) { return dateInRange(projectStart, 1000); } public static DateValidator dateInRange(final Date center, final int yearDiff) { return new DateValidator() { @Override public Pair<Boolean, String> apply(Date value) { int diff = Math.abs(value.getYear() - center.getYear()); if (diff > yearDiff) { return Pair.create(Boolean.FALSE, String.format( "Date %s is far away (%d years) from expected date %s. Any mistake?", value, diff, center)); } return Pair.create(Boolean.TRUE, null); } }; } } } private static Date tryParse(DateFormat dateFormat, String text) { try { return dateFormat.parse(text); } catch (ParseException e) { return null; } } public static ValueValidator<Date> createStringDateValidator(final DateValidator dv, final DateFormat... formats) { return new ValueValidator<Date>() { @Override public Date parse(String text) throws ValidationException { if (Strings.isNullOrEmpty(text)) { throw new ValidationException(); } Date parsed = null; for (DateFormat df : formats) { parsed = tryParse(df, text); if (parsed != null) { break; } } if (parsed == null) { throw new ValidationException("Can't parse value=" + text + "as date"); } if (dv != null) { Pair<Boolean, String> validationResult = dv.apply(parsed); if (!validationResult.first()) { throw new ValidationException(validationResult.second()); } } return parsed; } }; } public static void setupDatePicker( final JXDatePicker picker, final Date initialDate, final DateValidator dv, final ActionListener listener) { ValueValidator<Date> parseValidator = createStringDateValidator( dv, GanttLanguage.getInstance().getLongDateFormat(), GanttLanguage.getInstance().getShortDateFormat()); DatePickerEditCommiter commiter = setupDatePicker(picker, initialDate, dv, parseValidator, listener); commiter.attachOnFocusLost(listener); } // This class is responsible for committing user input in a text editor component of a date picker private static class DatePickerEditCommiter { private final JFormattedTextField myTextEditor; private final JXDatePicker myDatePicker; private final Date myInitialDate; private final DateValidator myDateValidator; private final ValueValidator<Date> myParseValidator; private DatePickerEditCommiter(JXDatePicker datePicker, JFormattedTextField textEditor, DateValidator dateValidator, ValueValidator<Date> parseValidator) { myTextEditor = Preconditions.checkNotNull(textEditor); myDatePicker = Preconditions.checkNotNull(datePicker); myInitialDate = myDatePicker.getDate(); myDateValidator = dateValidator; myParseValidator = parseValidator; } // We need listen focus lost in a dialog, but we don't need it in the table view, // so listener is optional void attachOnFocusLost(final ActionListener onSuccess) { myTextEditor.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { try { tryCommit(); onSuccess.actionPerformed(new ActionEvent(myDatePicker, ActionEvent.ACTION_PERFORMED, "")); return; } catch (ValidationException | ParseException ex) { // We probably don't want to log parse/validation exceptions // If user input is not valid we reset value to the initial one resetValue(); } } }); } void resetValue() { myTextEditor.setBackground(getValidFieldColor()); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (myInitialDate != null) { myDatePicker.setDate(myInitialDate); } } }); } // Tries to finish editing and set the result value into the date picker void tryCommit() throws ParseException, ValidationException { myTextEditor.commitEdit(); final Date dateValue; if (myTextEditor.getValue() instanceof Date) { if (myDateValidator != null) { Pair<Boolean, String> validation = myDateValidator.apply((Date)myTextEditor.getValue()); if (!validation.first()) { throw new ValidationException(validation.second()); } } dateValue = (Date) myTextEditor.getValue(); } else { dateValue = myParseValidator.parse(String.valueOf(myTextEditor.getText())); if (dateValue == null) { throw new ValidationException(); } } myDatePicker.setDate(dateValue); myTextEditor.setBackground(getValidFieldColor()); return; } } public static DatePickerEditCommiter setupDatePicker(final JXDatePicker picker, final Date initialDate, final DateValidator dv, final ValueValidator<Date> parseValidator, final ActionListener listener) { if (dv == null) { picker.addActionListener(listener); } else { picker.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Date date = ((JXDatePicker) e.getSource()).getDate(); if (date != null) { Pair<Boolean, String> validation = dv.apply(date); if (!validation.first()) { throw new ValidationException(validation.second()); } } } }); } final JFormattedTextField editor = picker.getEditor(); UIUtil.attachValidator(editor, parseValidator, null); if (initialDate != null) { picker.setDate(initialDate); } return new DatePickerEditCommiter(picker, editor, dv, parseValidator); } /** * @return a {@link JXDatePicker} component with the default locale, images * and date formats. */ public static JXDatePicker createDatePicker() { return createDatePicker(GanttLanguage.getInstance().getLongDateFormat(), GanttLanguage.getInstance().getShortDateFormat()); } public static JXDatePicker createDatePicker(DateFormat... dateFormats) { final JXDatePicker result = new JXDatePicker(); result.setLocale(GanttLanguage.getInstance().getDateFormatLocale()); result.setFormats(dateFormats); return result; } public static Dimension autoFitColumnWidth(JTable table, TableColumn tableColumn) { final int margin = 5; Dimension headerFit = getHeaderDimension(table, tableColumn); int width = headerFit.width; int height = 0; int order = table.convertColumnIndexToView(tableColumn.getModelIndex()); // Get maximum width of column data for (int r = 0; r < table.getRowCount(); r++) { TableCellRenderer renderer = table.getCellRenderer(r, order); Component comp = renderer.getTableCellRendererComponent(table, table.getValueAt(r, order), false, false, r, order); width = Math.max(width, comp.getPreferredSize().width); height += comp.getPreferredSize().height; } // Add margin width += 2 * margin; // Set the width return new Dimension(width, height); } public static Dimension getHeaderDimension(JTable table, TableColumn tableColumn) { TableCellRenderer renderer = tableColumn.getHeaderRenderer(); if (renderer == null) { renderer = table.getTableHeader().getDefaultRenderer(); } Component comp = renderer.getTableCellRendererComponent(table, tableColumn.getHeaderValue(), false, false, 0, 0); return comp.getPreferredSize(); } public static JComponent createButtonBar(JButton[] leftButtons, JButton[] rightButtons) { Box leftBox = Box.createHorizontalBox(); for (JButton button : leftButtons) { leftBox.add(button); leftBox.add(Box.createHorizontalStrut(3)); } Box rightBox = Box.createHorizontalBox(); for (JButton button : Lists.reverse(Arrays.asList(rightButtons))) { rightBox.add(Box.createHorizontalStrut(3)); rightBox.add(button); } JPanel result = new JPanel(new BorderLayout()); result.add(leftBox, BorderLayout.WEST); result.add(rightBox, BorderLayout.EAST); return result; } public static JComponent createTopAndCenter(JComponent top, JComponent center) { JPanel result = new JPanel(new BorderLayout()); top.setAlignmentX(Component.LEFT_ALIGNMENT); result.add(top, BorderLayout.NORTH); JPanel planePageWrapper = new JPanel(new BorderLayout()); planePageWrapper.setBorder(BorderFactory.createEmptyBorder(15, 0, 5, 0)); center.setAlignmentX(Component.LEFT_ALIGNMENT); planePageWrapper.add(center, BorderLayout.NORTH); result.add(planePageWrapper, BorderLayout.CENTER); return result; } public static JMenu createTooltiplessJMenu(Action action) { JMenu result = new JMenu(action) { @Override public JMenuItem add(Action a) { JMenuItem result = super.add(a); result.setToolTipText(null); return result; } }; result.setToolTipText(null); return result; } public static Color getValidFieldColor() { return UIManager.getColor("TextField.background"); } public static JEditorPane createHtmlPane(String html, HyperlinkListener hyperlinkListener) { JEditorPane htmlPane = new JEditorPane(); htmlPane.setEditorKit(new HTMLEditorKit()); htmlPane.setEditable(false); // htmlPane.setPreferredSize(new Dimension(400, 290)); htmlPane.addHyperlinkListener(hyperlinkListener); //htmlPane.setBackground(Color.YELLOW); htmlPane.setText(html); return htmlPane; } public static TableCellEditor newDateCellEditor(IGanttProject project, boolean showDatePicker) { return new GPDateCellEditor(project, showDatePicker, null, GanttLanguage.getInstance().getShortDateFormat()); } public static class GPDateCellEditor extends DefaultCellEditor implements ActionListener, GanttLanguage.Listener { private Date myDate; private final JXDatePicker myDatePicker; private final boolean myShowDatePicker; private DatePickerEditCommiter myCommitter; public GPDateCellEditor(IGanttProject project, boolean showDatePicker, ValueValidator<Date> parseValidator, DateFormat... dateFormats) { super(new JTextField()); myDatePicker = UIUtil.createDatePicker(dateFormats); myShowDatePicker = showDatePicker; if (parseValidator == null) { parseValidator = UIUtil.createStringDateValidator(null, dateFormats); } myCommitter = UIUtil.setupDatePicker(myDatePicker, null, null, parseValidator, getActionListener()); GanttLanguage.getInstance().addListener(this); } @Override public Component getTableCellEditorComponent(JTable arg0, Object value, boolean arg2, int arg3, int arg4) { if (value instanceof GanttCalendar) { myDatePicker.setDate(((GanttCalendar)value).getTime()); } return myShowDatePicker ? myDatePicker : myDatePicker.getEditor(); } private ActionListener getActionListener() { return this; } @Override public Object getCellEditorValue() { return CalendarFactory.createGanttCalendar(myDate == null ? new Date() : myDate); } @Override public boolean stopCellEditing() { try { myCommitter.tryCommit(); } catch (ValidationException | ParseException e) { myCommitter.resetValue(); return false; } myDate = myDatePicker.getDate(); getComponent().setBackground(null); super.fireEditingStopped(); return true; } @Override public void actionPerformed(ActionEvent e) { stopCellEditing(); } public void languageChanged(Event event) { myDatePicker.setFormats(GanttLanguage.getInstance().getShortDateFormat()); } } public static JComponent newColorComponent(Color value) { JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); final JPanel label = new JPanel(); label.setPreferredSize(new Dimension(16, 16)); label.setBackground(value); buttonPanel.add(label); // buttonPanel.add(new JXHyperlink(new AbstractAction("choose") { // public void actionPerformed(ActionEvent e) { // System.err.println("Clicked!"); // // } // })); return buttonPanel; } public static TableCellRenderer newColorRenderer(final Supplier<Integer> rowCount) { return new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { JComponent def = (JComponent) super.getTableCellRendererComponent(table, "", isSelected, hasFocus, row, column); if (row >= rowCount.get()) { return def; } JComponent buttonPanel = newColorComponent((Color)value); buttonPanel.setBackground(def.getBackground()); buttonPanel.setBorder(def.getBorder()); return buttonPanel; } }; } public static JComponent contentPaneBorder(JComponent component) { return border(component, 5, TOP | LEFT | BOTTOM | RIGHT); } public static final int TOP = 1, LEFT = 1 << 1, BOTTOM = 1 << 2, RIGHT = 1 << 3; public static JComponent border(JComponent component, int width, int mask) { component.setBorder(BorderFactory.createEmptyBorder( width * (mask & TOP), width * (mask & LEFT) >> 1, width * (mask & BOTTOM) >> 2, width * (mask & RIGHT) >> 3)); return component; } public static String formatPathForLabel(File file) { Path path = Paths.get(file.toURI()); if (path.getNameCount() <= 4) { return file.getAbsolutePath(); } Path prefix = path.subpath(0, 3); Path suffix = path.getFileName(); return path.getRoot().resolve(Paths.get(prefix.toString(), "...", suffix.toString())).toString(); } public static void setupErrorLabel(JLabel label, String errorMessage) { label.setIcon(GPAction.getIcon("8", "label-red-exclamation.png")); label.setText(errorMessage); label.setForeground(Color.RED); } public static void clearErrorLabel(JLabel label) { label.setIcon(null); label.setForeground(UIManager.getColor("Label.foreground")); } public static void initJavaFx(final Runnable andThen) { SwingUtilities.invokeLater(new Runnable() { public void run() { new JFXPanel(); // initializes JavaFX environment andThen.run(); } }); } public static class MultiscreenFitResult { public final double totalVisibleArea; public final double maxVisibleArea; public final GraphicsConfiguration argmaxVisibleArea; public MultiscreenFitResult(double totalVisibleArea, double maxVisibleArea, GraphicsConfiguration argmaxVisibleArea) { this.totalVisibleArea = totalVisibleArea; this.maxVisibleArea = maxVisibleArea; this.argmaxVisibleArea = argmaxVisibleArea; } } public static MultiscreenFitResult multiscreenFit(Rectangle bounds) { double visibleAreaTotal = 0.0; double maxVisibleArea = 0.0; GraphicsConfiguration argmaxVisibleArea = null; GraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment(); // Iterate through the screen devices and calculate how much of the stored // rectangle fits on every device. Also calculate the total visible area. for (GraphicsDevice gd : e.getScreenDevices()) { GraphicsConfiguration gc = gd.getDefaultConfiguration(); if (bounds.intersects(gc.getBounds())) { Rectangle visibleHere = bounds.intersection(gc.getBounds()); double visibleArea = 1.0 * visibleHere.height * visibleHere.width / (bounds.height * bounds.width); visibleAreaTotal += visibleArea; if (visibleArea > maxVisibleArea) { argmaxVisibleArea = gc; maxVisibleArea = visibleArea; } } } return new MultiscreenFitResult(visibleAreaTotal, maxVisibleArea, argmaxVisibleArea); } public static String getFontawesomeLabel(GPAction action) { if (action.getID() == null) { return null; } Object value = FONTAWESOME_PROPERTIES.get(action.getID()); return value == null ? null : String.valueOf(value); } public static boolean isFontawesomeSizePreferred() { String laf = UIManager.getLookAndFeel().getName().toLowerCase(); return laf.contains("macosx") || laf.contains("mac os x"); } public static float getFontawesomeScale(GPAction action) { float defaultScale = Float.valueOf(FONTAWESOME_PROPERTIES.get(".scale").toString()); if (action.getID() == null) { return defaultScale; } Object value = FONTAWESOME_PROPERTIES.get(action.getID() + ".scale"); return value == null ? defaultScale : defaultScale * Float.valueOf(value.toString()); } private static final float DEFAULT_YSHIFT = Float.valueOf(FONTAWESOME_PROPERTIES.get(".yshift").toString()); public static float getFontawesomeYShift(GPAction action) { if (action.getID() == null) { return DEFAULT_YSHIFT; } Object value = FONTAWESOME_PROPERTIES.get(action.getID() + ".yshift"); return DEFAULT_YSHIFT + (value == null ? 0f : Float.valueOf(value.toString())); } }