/* * Licensed Materials - Property of Perforce Software, Inc. * © Copyright Perforce Software, Inc. 2014, 2021 * © Copyright IBM Corp. 2009, 2014 * © Copyright ILOG 1996, 2009 * All Rights Reserved. * * Note to U.S. Government Users Restricted Rights: * The Software and Documentation were developed at private expense and * are "Commercial Items" as that term is defined at 48 CFR 2.101, * consisting of "Commercial Computer Software" and * "Commercial Computer Software Documentation", as such terms are * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4, * as applicable. */ import ilog.views.gantt.IlvDuration; import ilog.views.gantt.IlvGanttChart; import ilog.views.gantt.IlvTimeUtil; import ilog.views.gantt.action.IlvZoomToFitAction; import ilog.views.gantt.property.IlvActivityUserDefinedProperty; import ilog.views.gantt.swing.IlvConfigurableTableColumn; import ilog.views.gantt.swing.IlvJTable; import ilog.views.gantt.text.IlvDurationFormat; import ilog.views.util.IlvProductUtil; import ilog.views.util.IlvResourceUtil; import ilog.views.util.event.IlvEventListenerCollection; import ilog.views.util.event.IlvEventListenerList; import ilog.views.util.styling.IlvStylingException; import ilog.views.util.text.IlvDateFormatFactory; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.text.DateFormat; import java.util.*; import javax.swing.*; import javax.swing.event.CellEditorListener; import javax.swing.event.ChangeEvent; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; /** * Dynamic Column sample. * This sample code shows how to create table columns that display a * particular property of an <code>IlvGeneralActivity</code> without * requiring to code a custom subclass of <code>IlvStringColumn</code>. */ public class DynamicColumnSample extends JPanel { public IlvGanttChart chart; // The project configuration file that is used to load the general data model static final String projectURL = "data/data.igpr"; // The zoom-to-fit action. private IlvZoomToFitAction zoomToFitAction; // The resource bundle private ResourceBundle bundle; public DynamicColumnSample(JFrame frame) { // Load the resource bundles containing the locale dependent table // header labels for the gantt chart. bundle = IlvResourceUtil.getBundle("dynamicColumn.gantt", getLocale(), getClass().getClassLoader()); // Prepare the JPanel's appearance setLayout(new BorderLayout()); JPanel panel = new JPanel(); chart = new IlvGanttChart(); try { // Load the sample project chart.setProject(getResourceURL(projectURL)); } catch (IOException e1) { JOptionPane.showMessageDialog(frame, e1, "Error", JOptionPane.ERROR_MESSAGE); } catch (IlvStylingException e1) { JOptionPane.showMessageDialog(frame, e1, "Error", JOptionPane.ERROR_MESSAGE); } // A button to load the user-defined property columns. Button buttonGantt = new Button(bundle.getString("DynamicColumn.AddPropertyButton.Label")); buttonGantt.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { // Let's define the user-defined properties, and their types. // Note that this could be done by analysing the model. // The array of property names from the model that can be added. String[] propertyNames = { "department", "critical", "latestStart", "completion", "priority", "totalSlack" }; //The array of property classes. Class<?>[] propertyClasses = { String.class, Boolean.class, Date.class, Float.class, Integer.class, IlvDuration.class }; // Create and add a column to the table for each property. for (int i = 0; i < propertyNames.length; i++) { String property = propertyNames[i]; Class<?> propertyClass = propertyClasses[i]; IlvJTable table = chart.getTable(); // Add the column to the chart, if not already there. try { if (table.getColumn(property) != null) { // The column exists return; } } catch (IllegalArgumentException ex) { // the column does not exist. } // Create and add customized column, by doing the following actions: // 1. Create the user-defined property adapter that can be accessed // by the generic IlvStringProperty interface. // 2. Customize formatting for this property adapter based on the // property class. // 3. Create the configurable table column for this property adapter. // The table column can be customized with a table cell renderer, // a cell editor, and a width size. // 4. Add the table column to the table. // Get the string for the header value String headerValue = bundle.getString("DynamicColumn.HeaderColumn." + property); // Create the user-defined property adapter that can be accessed via // the generic IlvStringProperty interface. IlvActivityUserDefinedProperty userDefinedProperty = new IlvActivityUserDefinedProperty(property, propertyClass); // Customize formatting based on the property class if (propertyClass.isAssignableFrom(Date.class)) { DateFormat dateFormat = IlvDateFormatFactory.getDateInstance(DateFormat.DEFAULT, chart.getULocale()); userDefinedProperty.setFormat(dateFormat); } else if (propertyClass.isAssignableFrom(IlvDuration.class)) { IlvDurationFormat durationFormat = new IlvDurationFormat(IlvDurationFormat.TIME_UNIT_MEDIUM); durationFormat.setLenientParseMode(true); userDefinedProperty.setFormat(durationFormat); } // Create the configurable table column. IlvConfigurableTableColumn propertyColumn; // For the "priority" property use a slider as cell editor, and // render the value with different colors. if ("priority".equals(property)) { propertyColumn = new IlvConfigurableTableColumn(headerValue, userDefinedProperty, property, new SliderEditor(0, 10, 0), new PriorityRenderer()); } else if ("totalSlack".equals(property)) { // For the "totalSlack" property, use a larger column. propertyColumn = new IlvConfigurableTableColumn(headerValue, userDefinedProperty, 150, property); } else { propertyColumn = new IlvConfigurableTableColumn(headerValue, userDefinedProperty, property); } // Add the column to the table. table.addColumn(propertyColumn); } } }); panel.add(buttonGantt); add(BorderLayout.CENTER, chart); chart.revalidate(); zoomToFitAction = new IlvZoomToFitAction(chart, "", null, null, "", ""); zoomToFit(); // Make sure the full chart is visible. chart.expandAllRows(); // Set the initially displayed time span to start 1 day before the beginning // of the chart and extend for 3 weeks. Date visibleStartTime = IlvTimeUtil.subtract(chart.getGanttModel().getRootActivity().getStartTime(), IlvDuration.ONE_DAY); chart.setVisibleTime(visibleStartTime); chart.setVisibleDuration(IlvDuration.ONE_WEEK.multiply(3)); add(BorderLayout.SOUTH, panel); } public static void main(String[] args) { // This sample uses JViews Gantt features. When deploying an // application that includes this code, you need to be in possession // of a Perforce JViews Gantt Deployment license. IlvProductUtil.DeploymentLicenseRequired( IlvProductUtil.JViews_Gantt_Deployment); SwingUtilities.invokeLater(new Runnable() { Override public void run() { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setBounds(10, 400, 700, 400); DynamicColumnSample example = new DynamicColumnSample(frame); frame.getContentPane().add(example); frame.setVisible(true); } }); } // ========================================= // Accessing Resources // ========================================= /** * Returns the fully qualified URL of a resource file that is specified * relative to the working directory of the example. Note that this is * different than calling <code>Class.getResource()</code>, which performs * resource lookup relative to the example's classpath. * * @param relativePath The path of the resource file, relative to the working * directory of the example. * @return The resource URL. * * @throws IOException if the path cannot be resolved to a valid and existing * URL. */ public URL getResourceURL(String relativePath) throws IOException { relativePath = relativePath.replace('\\', '/'); URL url; // In an application context, we try to find an external file relative to // the current working directory. If an external file does not exist, then // the file may be bundled into the jar with the classes. In this case, we // prepend a '/' to relativePath so that we search relative to the // classloader's root and not relative to the packaging of the current // class. File file = new File(relativePath); // If the file exists in the external file system, we return its // corresponding URL. if (file.exists()) { url = file.toURI().toURL(); } // Otherwise, we search for the file relative to the classloader's root. // This will find the file if it is packaged into a jar with the classes. else { // Prepend a '/' so that we search relative to the classloader's root and // not relative to the packaging of the current class. if (relativePath.charAt(0) != '/') { relativePath = '/' + relativePath; } url = getClass().getResource(relativePath); } // Verify that we have a valid URL by trying to open its associated stream. if (url == null) { throw new FileNotFoundException(relativePath); } InputStream stream = url.openStream(); stream.close(); return url; } /** * Zooms the chart to fit the data model's time interval. */ protected void zoomToFit() { zoomToFitAction.perform(); } // ========================================= // PriorityColumn.SliderEditor // ========================================= /** * <code>SliderEditor</code> is a JSlider table cell editor that can be used * to edit an Integer of an <code>IlvGeneralActivity</code> within a table. */ static class SliderEditor extends JSlider implements TableCellEditor { /** * There is currently a bug in JSlider that causes refresh problems if we * add the slider directly to the table. Instead, the workaround is to put * the slider inside of a container and add the container to the table. */ private JPanel sliderContainer; /** * The list of cell editor listeners. */ private IlvEventListenerCollection<CellEditorListener> listeners = new IlvEventListenerList<CellEditorListener>(); /** * Creates a new horizontal <code>SliderEditor</code> . */ SliderEditor(int min, int max, int ini) { super(HORIZONTAL, min, max, ini); setPaintTicks(true); setMajorTickSpacing(1); setPaintLabels(true); setSnapToTicks(true); // Create custom tick labels for even values. Font labelFont = new Font("Dialog", Font.PLAIN, 10); Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>(); for (int i = 0; i <= max; i += 2) { JLabel label = new JLabel("" + i, JLabel.CENTER); label.setFont(labelFont); labels.put(i, label); } setLabelTable(labels); // Put the slider inside a simple panel container. The container will // be added to the table when editing starts, instead of the slider being // added directly. This works around a current Swing bug in JSlider. sliderContainer = new JPanel(new BorderLayout()); sliderContainer.add(this, BorderLayout.CENTER); } /** * Returns the component that should be added to the table's component * hierarchy to perform editing. Once installed in the table hierarchy this * component will be able to draw and receive user input. * * <p>Instead of returning the slider, we return the lightweight container that * the slider is in. This is a workaround for a current bug in JSlider.</p> * * @param table The JTable that is asking the editor to edit. This * parameter can be <code>null</code>. * @param value The value of the cell to be edited. This should either * be a String for a valid priority value or * <code>null</code>. * @param isSelected The cell is to be rendered with selection * highlighting. * @param row The row of the cell being edited. * @param column The column of the cell being edited. * @return The component to edit. */ Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { setValue(Integer.parseInt((String) value)); // Remove the comment from the following line for the slider to have the // same color as the table. // this.setBackground(table.getBackground()); //return sliderContainer; return this; } /** * Returns the value contained in the editor. This will be the slider's * value as a String. */ Override public Object getCellEditorValue() { return String.valueOf(getValue()); } /** * Asks the editor whether it can start editing using * <code>anEvent</code>. * <code>anEvent</code> is in the invoking component coordinate system. * The editor cannot assume the component returned by * <code>getCellEditorComponent()</code> is installed. This method is * intended for the use of the client to avoid the cost of setting up * and installing the editor component if editing is not possible. * If editing can be started this method returns <code>true</code>. * * @param anEvent The event the editor should use to consider whether to * begin editing. * @return <code>true</code> if editing can be started. * * @see #shouldSelectCell */ Override public boolean isCellEditable(EventObject anEvent) { if (anEvent instanceof MouseEvent) { if (((MouseEvent) anEvent).getClickCount() >= 2) { return true; } } return false; } /** * Specifies to the editor to start editing using <code>anEvent</code>. * The editor itself then decides whether it wants to start editing in * different states depending on the exact type of <code>anEvent</code>. * For example, with a text field editor, if the event is a mouse event * the editor might start editing with the cursor at the clicked point. If * the event is a keyboard event, it might want to replace the value * of the text field with that first key, and so on. <code>anEvent</code> * is in the invoking component's coordinate system. A <code>null</code> * value is a valid parameter for <code>anEvent</code>, and it is up to the * editor to determine what is the default starting state. For example, * a text field editor might want to select all the text and start * editing if <code>anEvent</code> is <code>null</code>. The editor can * assume the component returned by <code>getCellEditorComponent()</code> * is properly installed in the client component hierarchy before this * method is called. * <p> * The return value of <code>shouldSelectCell()</code> is a boolean * indicating whether the editing cell should be selected. Typically, * the return value is <code>true</code>, because in most cases the editing * cell should be selected. However, it is useful to return * <code>false</code> to keep the selection from changing for some types * of edits. For example, in a table that * contains a column of check boxes, the user might want to change * these check boxes without altering the selection. (See Netscape * Communicator for such an example.) Of course, it is up to * the client of the editor to use the return value, but it does not * need to if it does not want to. * * @param anEvent The event the editor should use to start editing. * @return <code>true</code> if the editor would like the editing cell to * be selected. * @see #isCellEditable */ Override public boolean shouldSelectCell(EventObject anEvent) { return true; } /** * Specifies to the editor to stop editing and accept any partially edited * value as the value of the editor. The editor returns <code>false</code> * if editing was not stopped. This is useful for editors that validate and * cannot accept invalid entries. * * @return <code>true</code> if editing was stopped. */ Override public boolean stopCellEditing() { fireEditingStopped(); return true; } /** * Specifies to the editor to cancel editing and not accept any partially * edited value. */ Override public void cancelCellEditing() { fireEditingCanceled(); } /** * Adds a listener to the list that is notified when the editor starts, * stops, or cancels editing. * * @param l The <code>CellEditorListener</code>. */ Override public void addCellEditorListener(CellEditorListener l) { listeners.addListener(l); } /** * Removes a listener from the list that is notified. * * @param l The <code>CellEditorListener</code>. */ Override public void removeCellEditorListener(CellEditorListener l) { listeners.removeListener(l); } /** * Notifies all registered listeners that editing has stopped. */ protected void fireEditingStopped() { Iterator<CellEditorListener> i = listeners.getListeners(); if (i.hasNext()) { ChangeEvent e = new ChangeEvent(this); while (i.hasNext()) { i.next().editingStopped(e); } } } /** * Notifies all registered listeners that editing has been cancelled. */ protected void fireEditingCanceled() { Iterator<CellEditorListener> i = listeners.getListeners(); if (i.hasNext()) { ChangeEvent e = new ChangeEvent(this); while (i.hasNext()) { i.next().editingCanceled(e); } } } } // ========================================= // Priority Column TableCellRenderer // ========================================= /** * Creates a renderer that will be used to render the cells in this column. For each * cell, the renderer will be passed the <code>String</code> <em>value</em> returned by * the column's {@link IlvConfigurableTableColumn#getValue} method. We start with a * basic Swing <code>DefaultTableCellRenderer</code> and override the * <code>getTableCellRendererComponent</code> method to set the text color based on the * priority level. */ static class PriorityRenderer extends DefaultTableCellRenderer { private Color darkGreen = Color.green.darker(); PriorityRenderer() { setHorizontalAlignment(JLabel.CENTER); } Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (value instanceof String && ((String) (value)).length() != 0) { int intVal = Integer.parseInt((String) value); Color color; if (intVal <= 2) { color = Color.red; } else if (intVal <= 4) { color = Color.orange; } else if (intVal <= 7) { color = darkGreen; } else { color = Color.blue; } comp.setForeground(color); } return comp; } } }