/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, Version 3.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.gnu.org/licenses/agpl.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package se.streamsource.streamflow.client.ui.workspace.table; import ca.odell.glazedlists.SeparatorList; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; import ca.odell.glazedlists.gui.TableFormat; import ca.odell.glazedlists.swing.EventJXTableModel; import ca.odell.glazedlists.swing.EventTableModel; import com.jgoodies.forms.builder.DefaultFormBuilder; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import org.jdesktop.application.ApplicationContext; import org.jdesktop.swingx.JXTable; import org.jdesktop.swingx.color.ColorUtil; import org.jdesktop.swingx.decorator.AbstractHighlighter; import org.jdesktop.swingx.decorator.HighlightPredicate; import org.jdesktop.swingx.decorator.HighlighterFactory; import org.jdesktop.swingx.renderer.DefaultTableRenderer; import org.jdesktop.swingx.renderer.StringValue; import org.jdesktop.swingx.table.TableColumnExt; import org.jdesktop.swingx.table.TableColumnModelExt; import org.qi4j.api.common.Optional; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.Uses; import org.qi4j.api.structure.Module; import org.qi4j.api.util.Iterables; import se.streamsource.dci.value.link.LinkValue; import se.streamsource.streamflow.api.administration.priority.PriorityValue; import se.streamsource.streamflow.api.workspace.cases.CaseStates; import se.streamsource.streamflow.client.Icons; import se.streamsource.streamflow.client.MacOsUIWrapper; import se.streamsource.streamflow.client.ui.DateFormats; import se.streamsource.streamflow.client.ui.workspace.WorkspaceResources; import se.streamsource.streamflow.client.ui.workspace.cases.CaseResources; import se.streamsource.streamflow.client.ui.workspace.cases.CaseTableValue; import se.streamsource.streamflow.client.util.CommandTask; import se.streamsource.streamflow.client.util.RefreshWhenShowing; import se.streamsource.streamflow.client.util.i18n; import se.streamsource.streamflow.client.util.table.SeparatorTable; import se.streamsource.streamflow.infrastructure.event.domain.DomainEvent; import se.streamsource.streamflow.infrastructure.event.domain.TransactionDomainEvents; import se.streamsource.streamflow.infrastructure.event.domain.source.TransactionListener; import se.streamsource.streamflow.infrastructure.event.domain.source.helper.EventParameters; import se.streamsource.streamflow.infrastructure.event.domain.source.helper.Events; import se.streamsource.streamflow.util.Strings; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.awt.font.TextAttribute; import java.awt.geom.Ellipse2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Set; import java.util.TimeZone; import static java.awt.RenderingHints.*; import static java.lang.Integer.*; import static se.streamsource.streamflow.client.util.i18n.*; import static se.streamsource.streamflow.infrastructure.event.domain.source.helper.Events.*; /** * Base class for all views of case lists. */ public class CasesTableView extends JPanel implements TransactionListener { @Structure Module module; public static final int MILLIS_IN_DAY = (1000 * 60 * 60 * 24); public static final WorkspaceResources[] dueGroups = {WorkspaceResources.overdue, WorkspaceResources.duetoday, WorkspaceResources.duetomorrow, WorkspaceResources.duenextweek, WorkspaceResources.duenextmonth, WorkspaceResources.later, WorkspaceResources.noduedate}; private Comparator<CaseTableValue> groupingComparator = new Comparator<CaseTableValue>() { public int compare( CaseTableValue o1, CaseTableValue o2 ) { GroupBy groupBy = model.getGroupBy(); switch (groupBy) { case caseType: return o1.caseType().get().compareTo( o2.caseType().get() ); case dueOn: return dueOnGroup( o1.dueOn().get() ).compareTo( dueOnGroup( o2.dueOn().get() ) ); case assignee: return o1.assignedTo().get().compareTo( o2.assignedTo().get() ); case project: return o1.owner().get().compareTo( o2.owner().get() ); case priority: Integer prio1 = o1.priority().get() != null ? o1.priority().get().priority().get() : new Integer( 9999 ); Integer prio2 = o2.priority().get() != null ? o2.priority().get().priority().get() : new Integer( 9999 ); return prio1.compareTo( prio2 ); default: return 0; } } }; protected JXTable caseTable; protected CasesTableModel model; private TableFormat tableFormat; private ApplicationContext context; private PerspectiveView filter; public void init( final @Service ApplicationContext context, @Uses CasesTableModel casesTableModel, final @Uses TableFormat tableFormat, @Optional @Uses JTextField searchField ) { setLayout( new BorderLayout() ); this.context = context; this.model = casesTableModel; this.tableFormat = tableFormat; ActionMap am = context.getActionMap( CasesTableView.class, this ); setActionMap( am ); MacOsUIWrapper.convertAccelerators( context.getActionMap( CasesTableView.class, this ) ); // Filter filter = module.objectBuilderFactory().newObjectBuilder(PerspectiveView.class).use( model, searchField ).newInstance(); add( filter, BorderLayout.NORTH ); // Table // Trigger creation of filters and table model caseTable = new SeparatorTable( null ) { public Component prepareRenderer( TableCellRenderer renderer, int row, int column) { Component c = super.prepareRenderer(renderer, row, column); // add custom rendering here EventTableModel model = (EventTableModel) getModel(); if( model.getElementAt( row ) instanceof CaseTableValue ) { Map attributes = c.getFont().getAttributes(); if( ((CaseTableValue) model.getElementAt( row )).removed().get() ) { attributes.put( TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); } else if ( ((CaseTableValue) model.getElementAt( row )).unread().get() ) { attributes.put( TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD ); } c.setFont( new Font(attributes) ); } return c; } }; caseTable.setSelectionMode( ListSelectionModel.SINGLE_SELECTION ); caseTable.getActionMap().getParent().setParent( am ); caseTable.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, KeyboardFocusManager.getCurrentKeyboardFocusManager() .getDefaultFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS ) ); caseTable.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, KeyboardFocusManager.getCurrentKeyboardFocusManager() .getDefaultFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS ) ); caseTable.getActionMap().remove( "column.horizontalScroll" ); caseTable.getActionMap().remove( "column.packAll" ); caseTable.getActionMap().remove( "column.packSelected" ); caseTable.setColumnControlVisible( true ); caseTable.setModel( new EventJXTableModel<CaseTableValue>( model.getEventList(), tableFormat ) ); model.addObserver( new Observer() { public void update( Observable o, Object arg ) { if (model.getGroupBy() == GroupBy.none) { caseTable.setModel( new EventJXTableModel<CaseTableValue>( model.getEventList(), tableFormat ) ); } else { SeparatorList<CaseTableValue> groupingList = new SeparatorList<CaseTableValue>( model.getEventList(), groupingComparator, 1, 10000 ); caseTable.setModel( new EventJXTableModel<CaseTableValue>( groupingList, tableFormat ) ); } if( !model.containsCaseWithPriority()) { model.addInvisibleColumn( 8 ); } for (Integer invisibleCol : model.getInvisibleColumns()) { TableColumnModelExt tm = (TableColumnModelExt) caseTable.getColumnModel(); if (tm.getColumnExt( invisibleCol ).isVisible()) caseTable.getColumnExt( invisibleCol ).setVisible( false ); } } } ); caseTable.getColumn( 0 ).setPreferredWidth( 500 ); caseTable.getColumn( 1 ).setPreferredWidth( 70 ); caseTable.getColumn( 1 ).setMaxWidth( 70 ); caseTable.getColumn( 1 ).setResizable( false ); caseTable.getColumn( 2 ).setPreferredWidth( 300 ); caseTable.getColumn( 2 ).setMaxWidth( 300 ); caseTable.getColumn( 3 ).setPreferredWidth( 150 ); caseTable.getColumn( 3 ).setMaxWidth( 150 ); caseTable.getColumn( 4 ).setPreferredWidth( 90 ); caseTable.getColumn( 4 ).setMaxWidth( 90 ); caseTable.getColumn( 5 ).setPreferredWidth( 150 ); caseTable.getColumn( 5 ).setMaxWidth( 150 ); caseTable.getColumn( 6 ).setPreferredWidth( 90 ); caseTable.getColumn( 6 ).setMaxWidth( 90 ); caseTable.getColumn( 7 ).setPreferredWidth( 150 ); caseTable.getColumn( 7 ).setMaxWidth( 150 ); caseTable.getColumn( 7 ).setResizable( false ); caseTable.getColumn( 8 ).setPreferredWidth( 100 ); caseTable.getColumn( 8 ).setMaxWidth( 100 ); caseTable.getColumn( 9 ).setMaxWidth( 50 ); caseTable.getColumn( 9 ).setResizable( false ); caseTable.setAutoCreateColumnsFromModel( false ); int count = 0; for (TableColumn c : caseTable.getColumns()) { c.setIdentifier( (Integer)count ); count++; c.addPropertyChangeListener( new PropertyChangeListener() { public void propertyChange( PropertyChangeEvent evt ) { if ("visible".equals( evt.getPropertyName() )) { TableColumnExt columnExt = (TableColumnExt) evt.getSource(); if (columnExt.isVisible()) { model.removeInvisibleColumn( columnExt.getModelIndex() ); } else { model.addInvisibleColumn( columnExt.getModelIndex() ); } } } } ); } JScrollPane caseScrollPane = new JScrollPane( caseTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); add( caseScrollPane, BorderLayout.CENTER ); caseTable.setDefaultRenderer( Date.class, new DefaultTableRenderer( new StringValue() { private static final long serialVersionUID = 4782416330896582518L; public String getString( Object value ) { return value != null ? DateFormats.getProgressiveDateTimeValue( (Date) value, Locale.getDefault() ) : ""; } } ) ); caseTable.setDefaultRenderer( ArrayList.class, new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column ) { if (value == null) return this; if (value instanceof SeparatorList.Separator) return caseTable.getDefaultRenderer( SeparatorList.Separator.class ) .getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); JPanel renderer = new JPanel( new FlowLayout( FlowLayout.LEFT ) ); ArrayList<String> icons = (ArrayList<String>) value; for (String icon : icons) { ImageIcon image = i18n.icon( Icons.valueOf( icon ), 11 ); JLabel iconLabel = image != null ? new JLabel( image, SwingConstants.LEADING ) : new JLabel( " " ); renderer.add( iconLabel ); } if (isSelected) renderer.setBackground( table.getSelectionBackground() ); return renderer; } } ); caseTable.setDefaultRenderer( CaseStates.class, new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column ) { if( value == null ) return this; EventTableModel model = (EventTableModel) table.getModel(); boolean hasResolution = !Strings.empty( ((CaseTableValue) model.getElementAt( row )).resolution().get() ); boolean removed = ((CaseTableValue)model.getElementAt( row )).removed().get(); String iconName = hasResolution ? "case_status_withresolution_" + value.toString().toLowerCase() + "_icon" : "case_status_" + value.toString().toLowerCase() + "_icon"; iconName = removed ? "case_status_draft_icon" : iconName; JLabel renderedComponent = (JLabel) super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); renderedComponent.setHorizontalAlignment( SwingConstants.CENTER ); setText( null ); setIcon( i18n.icon( CaseResources.valueOf( iconName ), i18n.ICON_16 ) ); setName( i18n.text( CaseResources.valueOf( "case_status_" + value.toString().toLowerCase() + "_text" ) ) ); setToolTipText( i18n.text( CaseResources.valueOf( "case_status_" + value.toString().toLowerCase() + "_text" ) ) ); return this; } } ); caseTable.setDefaultRenderer( PriorityValue.class, new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column ) { final PriorityValue priority = (PriorityValue) value; String val = priority == null ? "" : priority.text().get(); JPanel panel = new JPanel( ); FormLayout layout = new FormLayout( "10dlu, 50dlu:grow", "pref" ); DefaultFormBuilder formBuilder = new DefaultFormBuilder( layout, panel ); panel.setBorder( BorderFactory.createEmptyBorder( 0, 0, 0, 0 ) ); JLabel label = new JLabel( ){ @Override protected void paintComponent(Graphics g) { Color color = getBackground(); if( priority != null ) { if( !Strings.empty( priority.color().get() ) ) color = new Color( parseInt( priority.color().get() ) ); else color = Color.BLACK; } final Color FILL_COLOR = ColorUtil.removeAlpha( color ); Graphics2D g2 = (Graphics2D) g.create(); try { g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); g2.setColor(Color.LIGHT_GRAY); final int DIAM = Math.min(getWidth(), getHeight()); final int inset = 3; g2.fill(new Ellipse2D.Float(inset, inset, DIAM-2*inset, DIAM-2*inset)); g2.setColor(FILL_COLOR); final int border = 1; g2.fill(new Ellipse2D.Float(inset+border, inset+border, DIAM-2*inset-2*border, DIAM-2*inset-2*border)); } finally { g2.dispose(); } } }; label.setPreferredSize( new Dimension( 10, 10 ) ); //label.setBorder( BorderFactory.createLineBorder( Color.RED ) ); formBuilder.add( ( Strings.empty(val) || "-".equals( val ) ) ? new JLabel( ) : label, new CellConstraints(1 , 1, 1, 1, CellConstraints.FILL, CellConstraints.FILL, new Insets( 0, 0, 0, 0 ) ) ); JLabel text = new JLabel( val ); //text.setBorder( BorderFactory.createLineBorder( Color.RED ) ); formBuilder.add( text, new CellConstraints(2, 1, 1, 1, CellConstraints.LEFT, CellConstraints.FILL, new Insets( 0, 0, 0, 0 ) ) ); if (isSelected) { panel.setBackground( table.getSelectionBackground() ); text.setForeground( table.getSelectionForeground() ); } return panel; } }); caseTable.setDefaultRenderer( SeparatorList.Separator.class, new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent( JTable table, Object separator, boolean isSelected, boolean hasFocus, int row, int column ) { String value = ""; boolean emptyDescription = false; switch (model.getGroupBy()) { case caseType: emptyDescription = Strings.empty( ((CaseTableValue) ((SeparatorList.Separator) separator).first()).caseType().get() ); value = !emptyDescription ? ((CaseTableValue) ((SeparatorList.Separator) separator).first()).caseType().get() : text( WorkspaceResources.no_casetype ); break; case assignee: emptyDescription = Strings.empty( ((CaseTableValue) ((SeparatorList.Separator) separator).first()).assignedTo().get() ); value = !emptyDescription ? ((CaseTableValue) ((SeparatorList.Separator) separator).first()).assignedTo().get() : text( WorkspaceResources.no_assignee ); break; case project: emptyDescription = Strings.empty( ((CaseTableValue) ((SeparatorList.Separator) separator).first()).owner().get() ); value = !emptyDescription ? ((CaseTableValue) ((SeparatorList.Separator) separator).first()).owner().get() : text( WorkspaceResources.no_project ); break; case dueOn: value = text( dueGroups[dueOnGroup( ((CaseTableValue) ((SeparatorList.Separator) separator).first()).dueOn().get() )] ); break; case priority: emptyDescription = ((CaseTableValue) ((SeparatorList.Separator) separator).first()).priority().get() == null || Strings.empty( ((CaseTableValue) ((SeparatorList.Separator) separator).first()).priority().get().color().get() ); value = !emptyDescription ? ((CaseTableValue) ((SeparatorList.Separator) separator).first()).priority().get().text().get() : text( WorkspaceResources.no_priority); break; } Component component = super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); component.setFont( component.getFont().deriveFont( Font.BOLD + Font.ITALIC ) ); component.setBackground( Color.lightGray ); return component; } } ); AbstractHighlighter separatorHighlighter = (AbstractHighlighter) HighlighterFactory.createSimpleStriping(HighlighterFactory.QUICKSILVER); separatorHighlighter.setHighlightPredicate(new HighlightPredicate.TypeHighlightPredicate(SeparatorList.Separator.class)); caseTable.addHighlighter(HighlighterFactory.createAlternateStriping()); caseTable.addHighlighter(separatorHighlighter); addFocusListener( new FocusAdapter() { public void focusGained( FocusEvent e ) { caseTable.requestFocusInWindow(); } } ); model.getEventList().addListEventListener( new ListEventListener<CaseTableValue>() { public void listChanged( ListEvent<CaseTableValue> listChanges ) { // Synchronize lists Set<String> labels = new HashSet<String>(); Set<String> assignees = new HashSet<String>(); Set<String> projects = new HashSet<String>(); for (CaseTableValue caseTableValue : listChanges.getSourceList()) { for (LinkValue linkValue : caseTableValue.labels().get().links().get()) { labels.add( linkValue.text().get() ); } assignees.add( caseTableValue.assignedTo().get() ); projects.add( caseTableValue.owner().get() ); } List<String> sortedLabels = new ArrayList<String>( labels ); List<String> sortedAssignees = new ArrayList<String>( assignees ); List<String> sortedProjects = new ArrayList<String>( projects ); Collections.sort( sortedLabels ); Collections.sort( sortedAssignees ); Collections.sort( sortedProjects ); sortedLabels.add( 0, text( WorkspaceResources.all ) ); sortedAssignees.add( 0, text( WorkspaceResources.all ) ); sortedProjects.add( 0, text( WorkspaceResources.all ) ); } } ); addHierarchyListener(new HierarchyListener() { public void hierarchyChanged(HierarchyEvent e) { if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) > 0) { if (CasesTableView.this.isShowing()) { context.getActionMap().get("savePerspective").setEnabled(true); } } } }); new RefreshWhenShowing( this, model ); } public JXTable getCaseTable() { return caseTable; } public CasesTableModel getModel() { return model; } private Integer dueOnGroup( Date date ) { /** * 0 = Overdue * 1 = Today * 2 = Tomorrow * 3 = Within next week * 4 = Within next month * 5 = Later * 6 = No due date */ long currentTime = System.currentTimeMillis(); currentTime = currentTime / MILLIS_IN_DAY; currentTime *= MILLIS_IN_DAY; Date today = new Date( currentTime ); Date lateToday = new Date( currentTime + MILLIS_IN_DAY - 1 ); Calendar month = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); month.setTime( today ); month.add( Calendar.MONTH, 1 ); Calendar week = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); week.setTime( today ); week.add( Calendar.WEEK_OF_YEAR, 1 ); Calendar tomorrow = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); tomorrow.setTime( lateToday ); tomorrow.add( Calendar.DATE, 1 ); int group; if (date == null) group = 6; else if (date.after( month.getTime() )) group = 5; // Later else if (date.after( week.getTime() )) group = 4; // Within next month else if (date.after( tomorrow.getTime() )) group = 3; // Within next week else if (date.after( lateToday )) group = 2; // Tomorrow else if (date.after( today )) group = 1; else group = 0; return group; } public void notifyTransactions( final Iterable<TransactionDomainEvents> transactions ) { if (Events.matches( withNames( "createdCase" ), transactions )) { final DomainEvent event = Iterables.first( Iterables.filter( withNames( "createdCase" ), Events.events( transactions ) ) );; context.getTaskService().execute( new CommandTask() { @Override protected void command() throws Exception { model.refresh(); } @Override protected void succeeded( Iterable<TransactionDomainEvents> transactionEventsIterable ) { super.succeeded( transactionEventsIterable ); TableModel model = caseTable.getModel(); boolean rowFound = false; for( int i=0, n=model.getRowCount(); i < n; i++ ) { if( model.getValueAt( i, model.getColumnCount() ).toString().endsWith( EventParameters.getParameter( event, "param1" ) + "/") ) { caseTable.getSelectionModel().setSelectionInterval( caseTable.convertRowIndexToView( i ), caseTable.convertRowIndexToView( i ) ); caseTable.scrollRectToVisible( caseTable.getCellRect( i, 0, true ) ); rowFound = true; break; } } } } ); } else if (Events.matches( withNames( "addedLabel", "removedLabel", "changedDescription", "changedCaseType", "changedStatus", "changedOwner", "assignedTo", "unassigned", "changedRemoved","deletedEntity", "updatedContact", "addedContact", "deletedContact", "createdConversation", "changedDueOn", "submittedForm", "createdAttachment", "removedAttachment", "changedPriority", "setUnread", "createdMessageFromDraft" ), transactions )) { context.getTaskService().execute( new CommandTask() { @Override protected void command() throws Exception { model.refresh(); if (Events.matches( withNames( "changedStatus", "changedOwner", "assignedTo", "unassigned", "deletedEntity" ), transactions )) { caseTable.getSelectionModel().clearSelection(); } } } ); } } }