package fr.lteconsulting.hexa.client.ui.miracle; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseDownHandler; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Widget; import fr.lteconsulting.hexa.client.ui.miracle.Edits.Editor; import fr.lteconsulting.hexa.client.ui.treetable.Row; import fr.lteconsulting.hexa.client.ui.treetable.TreeTable; import fr.lteconsulting.hexa.client.ui.treetable.event.TableCellClickEvent.TableCellClickHandler; import fr.lteconsulting.hexa.client.ui.treetable.event.TableHeaderClickEvent.TableHeaderClickHandler; public class DynTreeInTreeTable<T> implements Prints<Iterable<T>>, DynArrayManager<T> { public interface Resources { ImageResource treeMinus3(); ImageResource treePlus3(); ImageResource blank16(); } interface DefaultResources extends Resources, ClientBundle { @Override @Source( "images/treeMinus3.png" ) ImageResource treeMinus3(); @Override @Source( "images/treePlus3.png" ) ImageResource treePlus3(); @Override @Source( "images/blank16.png" ) ImageResource blank16(); } private static Resources defaultResources = null; private Resources resources = null; TreeTable table; TreeRefMng<T> refMng; ArrayList<ColumnMng<T>> columns = new ArrayList<ColumnMng<T>>(); Comparator<T> userComparator = null; // edition state private class EditionState { int editedCol = -1; T editedObject = null; Printer editedPrinter = null; Editor editedEditor = null; void close() { if( editedEditor != null ) editedEditor.close(); editedEditor = null; } } EditionState edition = null; public DynTreeInTreeTable( TreeTable table, TreeRefMng<T> refMng, Resources resources ) { if( resources == null ) { if( defaultResources == null ) defaultResources = GWT.create( DefaultResources.class ); this.resources = defaultResources; } else { this.resources = resources; } this.table = table; this.refMng = refMng; table.addTableHeaderClickHandler( tableHeaderClickHandler ); table.addTableCellClickHandler( tableCellClickHandler ); table.addDomHandler( onTableKeyUp, KeyDownEvent.getType() ); table.addDomHandler( onTableMouseDown, MouseDownEvent.getType() ); } public DynTreeInTreeTable( TreeTable table, TreeRefMng<T> refMng ) { this( table, refMng, null ); } public void addColumn( PrintsOn<T> column, Edits<T> editMng, CellClickMng<T> clickMng, PrintsOn<Void> hdrPrintsOn, CellClickMng<Void> hdrClickMng ) { columns.add( new ColumnMng<T>( column, editMng, clickMng, hdrPrintsOn, hdrClickMng ) ); } class HdrInTreeTablePrinter implements Printer { TreeTable table; int col; HdrInTreeTablePrinter( TreeTable table, int col ) { this.table = table; this.col = col; } @Override public void setText( String text ) { table.setHeader( col, text ); } @Override public void setHTML( String html ) { setText( html ); } @Override public void setWidget( Widget widget ) { assert false; table.setHeader( col, "WIDGET PLACE" ); } } class CellInTreeTablePrinter implements Printer { final Row item; final int col; CellInTreeTablePrinter( Row item, int col ) { this.item = item; this.col = col; } @Override public void setText( String text ) { item.setText( col, text ); } @Override public void setHTML( String html ) { item.setHTML( col, html ); } @Override public void setWidget( Widget widget ) { item.setWidget( col, widget ); } } @Override public void printHeaders() { table.setHeader( 0, "+/-" ); int nbCols = columns.size(); for( int i = 0; i < nbCols; i++ ) columns.get( i ).hdrPrintsOn.print( null, new HdrInTreeTablePrinter( table, i ) ); } @Override public void print( Iterable<T> data ) { killCurrentEdit(); table.emptyTable(); if( data == null ) return; // each row for( T d : data ) { InsertPos insPos = getInsertPoint( d ); Row row = insPos.insert(); row.setRef( refMng.getRef( d ) ); printRow( d, row ); } } @Override public void updateRow( T object ) { // find the row associated with this object int objectRef = refMng.getRef( object ); Row item = getRow( objectRef ); if( edition != null && refMng.getRef( edition.editedObject ) == objectRef ) killCurrentEdit(); // Item insPos = userComparator != null ? getInsertPoint( object ) : // item; InsertPos insPos = getInsertPoint( object ); // insert or move the row to the right place and create a cell printer if( item == null ) { // new item item = insPos.insert(); item.setRef( refMng.getRef( object ) ); } else { if( insPos.before != item ) { insPos.move( item ); } } // print the row printRow( object, item ); } class InsertPos { Row parent; Row before; void move( Row item ) { if( before != null ) item.moveBefore( before ); else item.moveLastChild( parent ); } Row insert() { if( before != null ) return before.addBefore(); Row item = table.addRow( parent ); if( parent != null ) { int ref = getRefAtRow( parent ); T obj = refMng.getObject( ref ); columns.get( 0 ).prints.print( obj, new PrinterFirstCol( parent ) ); } return item; } } private InsertPos getInsertPoint( T object ) { InsertPos res = new InsertPos(); // first get the parent T parent = refMng.getParentObject( object ); if( parent != null ) { res.parent = getRow( refMng.getRef( parent ) ); assert (res.parent != null) : "Cannot add a child of the parent is not firt added"; } ArrayList<Row> items = table.getItemChilds( res.parent ); int nbRows = items.size(); int objectRef = refMng.getRef( object ); if( userComparator != null ) { for( Row item : items ) { int refAtRow = item.getRef(); if( userComparator.compare( object, refMng.getObject( refAtRow ) ) <= 0 ) { if( refAtRow == objectRef ) { // keep the same place only if lesser than the nxt item // or if there is no next if( items.indexOf( item ) == nbRows - 1 ) { res.before = item; return res; } else { int refAtNextRow = getRefAtRow( item.getNextSiblingItem() ); if( userComparator.compare( object, refMng.getObject( refAtNextRow ) ) <= 0 ) { res.before = item; return res; } } } else { res.before = item; return res; } } } } return res; } @Override public void deleteRow( int ref ) { GWT.log( "DELETE ROW " + ref ); Row item = getRow( ref ); if( item == null ) return; // to reset the edition state, just in case... (an only if we are // editing on the row we delete) if( edition != null && refMng.getRef( edition.editedObject ) == ref ) killCurrentEdit(); Row parent = item.getParent(); GWT.log( "PARENT : " + parent ); GWT.log( "PARENT REF : " + (parent == null ? "-" : parent.getRef()) ); GWT.log( "BEFORE REMOVE " + ref ); item.remove(); GWT.log( "AFTER REMOVE " + ref ); if( parent != null ) { GWT.log( "PARENT HAS N CHILD : " + parent.getChilds().size() ); T obj = refMng.getObject( parent.getRef() ); columns.get( 0 ).prints.print( obj, new PrinterFirstCol( parent ) ); } } @Override public void setComparator( Comparator<T> comparator ) { userComparator = comparator; sortAndPrint( comparator ); } public void sortAndPrint( Comparator<T> userComparator ) { sortAndPrint( userComparator, null ); } // sort all children of parent public void sortAndPrint( final Comparator<T> userComparator, Row parent ) { class It { Row item; T object; It( Row item, T object ) { this.object = object; this.item = item; } } ; ArrayList<It> its = new ArrayList<It>(); // get all objects from the table ArrayList<Row> childs = table.getItemChilds( parent ); for( Row item : childs ) { int ref = getRefAtRow( item ); its.add( new It( item, refMng.getObject( ref ) ) ); } // sort them Collections.sort( its, new Comparator<It>() { @Override public int compare( It o1, It o2 ) { return userComparator.compare( o1.object, o2.object ); } } ); // now reorder the lines for( int j = 0; j < its.size(); j++ ) { InsertPos ip = getInsertPoint( its.get( j ).object ); if( ip.before != its.get( j ).item ) ip.move( its.get( j ).item ); } // proceed recursively for deeper level childs = table.getItemChilds( parent ); for( Row item : childs ) sortAndPrint( userComparator, item ); } private int getRefAtRow( Row item ) { return item.getRef(); } // return the row index of the row associated to the object referenced as // objectRef // returns null if the row is not found private Row getRow( int objectRef ) { return table.getItemForRef( objectRef ); } private void printRow( T object, Row row ) { // to reset the edition state, just in case... if( edition != null && refMng.getRef( edition.editedObject ) == refMng.getRef( object ) ) killCurrentEdit(); // writes the element reference on the corresponding row element // note that this does not create a hard link to the referenced object // so no garbage is created here row.setRef( refMng.getRef( object ) ); // print the icon corresponding to expand/shrink state columns.get( 0 ).prints.print( object, new PrinterFirstCol( row ) ); // each column for( int i = 1; i < columns.size(); i++ ) columns.get( i ).prints.print( object, new CellInTreeTablePrinter( row, i ) ); } class PrinterFirstCol implements Printer { final Row row; PrinterFirstCol( Row row ) { this.row = row; } @Override public void setText( String text ) { setHTML( text ); } @Override public void setHTML( String html ) { row.setHTML( 0, getExpShrinkHTML() + html ); } @Override public void setWidget( Widget widget ) { assert (false) : "NOT IMPLEMENTED : PUTTING WIDGETS IN THE FIRST COLUMN..."; } private String getExpShrinkHTML() { if( row.hasChilds() ) { if( row.getExpanded() ) return "<img src='" + resources.treeMinus3().getSafeUri().asString() + "'/>"; else return "<img src='" + resources.treePlus3().getSafeUri().asString() + "'/>"; } else return "<img src='" + resources.blank16().getSafeUri().asString() + "'/>"; } } private boolean beginEdit( Row item, int col ) { int ref = getRefAtRow( item ); if( edition != null && ref == refMng.getRef( edition.editedObject ) ) return true; // already began !!! killCurrentEdit(); Edits<T> editMng = columns.get( col ).edits; if( editMng == null ) return false; // find the T object that has been clicked T object = refMng.getObject( ref ); assert object != null; if( object == null ) return false; Element td = item.getTdElement( col ); edition = new EditionState(); // initialize and set the current edit edition.editedCol = col; edition.editedObject = object; // create a Printer corresponding to this cell edition.editedPrinter = new CellInTreeTablePrinter( item, col ); // create an editor and init it // -2 to remove padding : HACK HACK HACK edition.editedEditor = editMng.createEditor( edition.editedObject, edition.editedPrinter, onEdit, td.getOffsetWidth() - 2, td.getClientHeight() - 2 ); return true; } private static class CellPos { Row item; int col; CellPos( Row item, int col ) { this.item = item; this.col = col; } } private final MouseDownHandler onTableMouseDown = new MouseDownHandler() { @Override public void onMouseDown( MouseDownEvent event ) { Element source = DOM.eventGetTarget( Event.as( event.getNativeEvent() ) ); // try to see if it's not on a th element int hdr = table.getEventTargetHeaderIdx( source ); if( hdr < 0 ) return; DragDrop.initiate( source, onDragDrop, hdr, Event.as( event.getNativeEvent() ) ); } }; DragDrop.Callback<Integer> onDragDrop = new DragDrop.Callback<Integer>() { @Override public String getGhostInnerHTML( Integer cookie, Element source ) { // return // "<div style='background-color:rgba(120,120,120,0.4);'>Moving column "+cookie+"</div>"; class TempPrinter implements Printer { String html = null; @Override public void setWidget( Widget widget ) { assert false; } @Override public void setText( String text ) { html = text; } @Override public void setHTML( String html ) { this.html = html; } } TempPrinter printer = new TempPrinter(); columns.get( cookie ).hdrPrintsOn.print( null, printer ); // Due to asynchronism, it may be possible that the printer did not // yet print anything, well... Too late, we use the source element // instead !! if( printer.html == null ) return "<div style='background-color:rgba(120,120,120,0.4);'>" + source.getInnerHTML() + "</div>"; return "<div style='background-color:rgba(120,120,120,0.4);width:20px;height:20px;'>" + printer.html + "</div>"; } @Override public void onDragDropFinished( Integer cookie, Element source, Element destination ) { int newPos = table.getEventTargetHeaderIdx( destination ); if( newPos < 0 ) return; ColumnMng<T> dum = columns.get( cookie ); columns.set( cookie, columns.get( newPos ) ); columns.set( newPos, dum ); // Redraw headers printHeaders(); // Redraw all objects in the table redrawAll( null ); } }; private void redrawAll( Row parent ) { ArrayList<Row> items = table.getItemChilds( parent ); for( Row row : items ) { int ref = row.getRef(); printRow( refMng.getObject( ref ), row ); redrawAll( row ); } } private final Edits.Callback onEdit = new Edits.Callback() { @Override public void cancelEdition() { killCurrentEdit(); } @Override public void validateEdition( boolean fJumpNext ) { // just forget the edition state, but don't redraw // that means we let the client continue drawing on its cell if( edition != null && fJumpNext ) { CellPos next = getNextPos( getRow( refMng.getRef( edition.editedObject ) ), edition.editedCol ); edition.close(); edition = null; beginEdit( next.item, next.col ); } } }; // wait a bit to avoid ui jitter... // use refMng in case the data change during the time waited private void killCurrentEdit() { if( edition == null ) return; // reprints the cell, with uptodate data columns.get( edition.editedCol ).prints.print( refMng.getObject( refMng.getRef( edition.editedObject ) ), edition.editedPrinter ); edition.close(); edition = null; } CellPos getNextPos( Row item, int col ) { // either on the same row for( int c = col + 1; c < columns.size(); c++ ) { if( columns.get( c ).edits != null ) return new CellPos( item, c ); } // or on the next item = item.getNextTraversalItem(); for( int c = 0; c < columns.size(); c++ ) { if( columns.get( c ).edits != null ) return new CellPos( item, c ); } return null; } CellPos getPrevPos( Row item, int col ) { // either on the same row for( int c = col - 1; c >= 0; c-- ) { if( columns.get( c ).edits != null ) return new CellPos( item, c ); } // or on the previous item = item.getPrevTraversalItem(); for( int c = columns.size() - 1; c >= 0; c-- ) { if( columns.get( c ).edits != null ) return new CellPos( item, c ); } return null; } private final KeyDownHandler onTableKeyUp = new KeyDownHandler() { @Override public void onKeyDown( KeyDownEvent event ) { if( event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE ) { if( edition == null ) return; event.stopPropagation(); event.preventDefault(); killCurrentEdit(); } else if( event.getNativeKeyCode() == KeyCodes.KEY_TAB ) { if( edition == null ) return; event.stopPropagation(); event.preventDefault(); // find next cell to be edited Row editionItem = getRow( refMng.getRef( edition.editedObject ) ); CellPos pos = null; if( event.isShiftKeyDown() ) pos = getPrevPos( editionItem, edition.editedCol ); else pos = getNextPos( editionItem, edition.editedCol ); if( pos != null ) { beginEdit( pos.item, pos.col ); return; } } } }; private final TableHeaderClickHandler tableHeaderClickHandler = new TableHeaderClickHandler() { @Override public void onTableHeaderClick( int column, ClickEvent clickEvent ) { if( column < 0 ) return; CellClickMng<Void> clickMng = columns.get( column ).hdrClickMng; if( clickMng == null ) return; // get the printer, and go Printer printer = table.getHeaderPrinter( column ); clickMng.onTableClick( null, DOM.eventGetTarget( Event.as( clickEvent.getNativeEvent() ) ), printer ); } }; private final TableCellClickHandler tableCellClickHandler = new TableCellClickHandler() { @Override public void onTableCellClick( Row item, int column, ClickEvent event ) { int clickedRef = getRefAtRow( item ); // if clicking on a cell that is in editing mode, return if( edition != null && (refMng.getRef( edition.editedObject ) == clickedRef && edition.editedCol == column) ) return; killCurrentEdit(); if( column == 0 ) { // TODO : if clicked on the exp/shrink icon, do that // else, offer the possibility to edit the cell, or else send // the click event to click mng // expand / shrink item item.setExpanded( !item.getExpanded() ); T obj = refMng.getObject( clickedRef ); columns.get( 0 ).prints.print( obj, new PrinterFirstCol( item ) ); return; } boolean fHandled = false; // Tries edition fHandled = beginEdit( item, column ); // If not handled, tries click manager if( !fHandled ) { // if there is a click manager CellClickMng<T> clickMng = columns.get( column ).clicks; if( clickMng != null ) { // find the T object that has been clicked T object = refMng.getObject( clickedRef ); if( object == null ) return; CellInTreeTablePrinter pr = new CellInTreeTablePrinter( item, column ); fHandled = clickMng.onTableClick( object, DOM.eventGetTarget( Event.as( event.getNativeEvent() ) ), pr ); } } } }; @Override public void clearAllRows() { killCurrentEdit(); table.clear(); } }