001package fi.jyu.mit.fxgui;
002
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.HashMap;
007
008import javax.swing.SwingConstants;
009
010import javafx.application.Platform;
011import javafx.beans.property.ListProperty;
012import javafx.beans.property.SimpleListProperty;
013import javafx.beans.property.SimpleStringProperty;
014import javafx.beans.property.StringProperty;
015import javafx.collections.FXCollections;
016import javafx.collections.ListChangeListener;
017import javafx.collections.ObservableList;
018import javafx.fxml.FXML;
019import javafx.geometry.Pos;
020import javafx.scene.control.TableCell;
021import javafx.scene.control.TableColumn;
022import javafx.scene.control.TableRow;
023import javafx.scene.control.TableView;
024import javafx.scene.control.TextField;
025import javafx.scene.input.KeyCode;
026
027/**
028 * StringGrid joka näyttää merkkijonotaulukon sisällön.
029 * Taulukkoon voidaan jokaista riviä kohti tallentaa myös jokin olio.
030 * 
031 *  grid.add(har,rivi);  // rivillä sarakkeiden merkkijonot
032 * 
033 * Kullekin solulle voidaan antaa oma css-tyyli.  Ongelma: mikäli
034 * ei kuunnella hiiren klikkausta otsikossa ja tehdä refresh, niin tämä
035 * menee sekaisin jos rivit lajitellaan (jos eri riveillä eri css).
036 * TYPE saa olla ?, mikäli tallennettavia olioita ei käytetä mihinkään
037 * 
038 * Soluihin pääsee käsiksi alkuperäisen (lajittelemattoman) taulukon
039 * rivi- ja sarakeindekseillä.  Ellei erikseen mainita, parametreissa
040 * olevat row- ja col-indeksit ovat nimenomaan alkueräisiä indeksejä.
041 * 
042 * Dataa voidaan lisätä myös ilman merkkijonoja.  
043 * 
044 *  grid.add(jasenet);
045 * 
046 * Tällöin on vähintään kerrottavat miten merkkijonot saadaan tietylle riville ja
047 * sarekkeelle, esim tyyliin:
048 * 
049 *  grid.setOnCellString( (g, jasen, defValue, r, c) -> jasen.anna(c+eka) );
050 *     
051 * Mikäli halutaan lajitella sarakkeita muuta kuin merkkijonojärjestyksessä,
052 * on kerrottava lajittelumerkkijono tyyliin:
053 *     
054 *  grid.setOnCellValue( (g, jasen, defValue, r, c) -> jasen.getAvain(c+eka) );
055 *
056 * Katso myös: <a href="https://tim.jyu.fi/view/kurssit/tie/ohj2/tyokalut/JavaFX/fxgui/StringGrid">StringGrid TIMissä</a>
057 * 
058 * @param <TYPE> minkä tyyppisiä tietoja liitetään riveihin
059 *
060 * @author vesal
061 * @version 29.12.2015
062 * @version 25.3.2015/vl - TYPE mukaan ja lisää ominaisuuksia
063 */
064public class StringGrid<TYPE> extends TableView<StringGrid.GridRowItem<TYPE>> {
065    
066    /**
067     * Alkio yhdelle riville.
068     * Pidetään yllä itse olioita, listaa merkkijoista,
069     * alkuperäisestä rivinumerosta ja solu css:stä.
070     * @param <TYPE> mikä tieto tallennetaan riviin
071     */
072    @SuppressWarnings("javadoc")
073    public static class GridRowItem<TYPE> {
074        private ObservableList<String> rowSts;
075        private TYPE item;
076        private int rowNr;     
077        private HashMap<Integer, String> styleClasses;    
078        
079        /**
080         * @param row mikä teksti tallennetaan
081         * @param item mikä alkio tähämn kohti
082         * @param rowNr mikä on alkuperäinen rivinumero
083         */
084        public GridRowItem(String[] row, TYPE item, int rowNr) {
085            this.rowSts = FXCollections.observableArrayList();
086            this.setRow(row);
087            this.setItem(item);
088            this.setRowNr(rowNr);
089        }
090        
091        public void setItem(TYPE item) {
092            this.item = item;
093        }
094        
095        /**
096         * @return kohdalla oleva alkio
097         */
098        public TYPE getItem() {
099            return item;
100        }
101
102        public int getRowNr() {
103            return rowNr;
104        }
105
106        public void setRowNr(int rowNr) {
107            this.rowNr = rowNr;
108        }
109
110        public ObservableList<String> getRow() {
111            return rowSts;
112        }
113
114        public void setRow(String[] row) {
115            this.rowSts.setAll(row);
116        }
117
118        public String getCellClass(int col) {
119            if ( styleClasses == null ) return null;
120            return styleClasses.get(col);
121        }
122
123        public void setCellClass(String cellClass, int col) {
124            if ( styleClasses == null ) styleClasses = new HashMap<>();
125            styleClasses.put(col, cellClass);
126        }
127        
128        
129        public String get(int col) {
130            if ( col < 0 || col >= rowSts.size() ) return "";
131            return rowSts.get(col);
132        }
133        
134        public void set(int col, String s) {
135            if ( col <0 || col >= rowSts.size() ) return;
136            rowSts.set(col,s);
137        }
138
139    }
140
141    
142    /**
143     * Rajapinta solun muokkaukselle
144     * @param <TYPE> minkätyylisiä
145     */
146    public interface OnGridCell<TYPE> {
147        /**
148         * Metodi jota kutsutaan kun tarvitaan solun sisältöä
149         * @param grid taulukko jota käytetään
150         * @param item mikä alkio liitetty tähän
151         * @param defValue solulle ehdotettu arvo
152         * @param row taulukon rivi johon liittyy
153         * @param column taulukon sarake johon liittyy
154         * @return solulle haluttu arvo, null: käytetään oletusta
155         */
156        String onGetCell(StringGrid<TYPE> grid, TYPE item, String defValue, int row, int column);
157    }
158    
159    
160    /**
161     * Rajapinta solun editoinnille
162     * @param <TYPE> minkätyylisiä
163     */
164    public interface OnGridLiveEdit<TYPE> {
165        /**
166         * Metodi jota kutsutaan kun solua muokataan
167         * @param grid taulukko jota muokattiin
168         * @param item mikä alkio liitetty tähän
169         * @param newValue solulle ehdotettu uusi arvo
170         * @param row taulukon rivi jota muokattiin
171         * @param column taulukon sarake jota mmuokattiin
172         * @param textField tekstikenttä jota muokataan
173         * @return solulle haluttu arvo, null: ei muutosta
174         */
175        String onEdit(StringGrid<TYPE> grid, TYPE item, String newValue, int row, int column, TextField textField);
176    }
177    
178    
179    private ListProperty<GridRowItem<TYPE>> rivitProp = new SimpleListProperty<>();
180    private ObservableList<GridRowItem<TYPE>> tableRows = FXCollections.observableArrayList();
181    private StringProperty rivitJono = new SimpleStringProperty();
182    private String emptyStyleClass = null;
183    private HashMap<Integer,Pos> alignments = new HashMap<>(); 
184    
185    /** Käsittelijä muokkauksille */
186    protected OnGridCell<TYPE> onGridEdit;
187    /** Käsittelijä reaaliaikaisille muokkauksille */
188    protected OnGridLiveEdit<TYPE> onGridLiveEdit;
189    /** Käsittelijä lajitteluarvolle */
190    protected OnGridCell<TYPE> onCellValue;
191    /** Käsittelijä solun merkkijonolle */
192    protected OnGridCell<TYPE> onCellString;
193    
194    
195    /**
196     * Alustetaan taulukko
197     */
198    public StringGrid() {
199        setItems(tableRows);
200    }
201    
202    
203    /**
204     * @return mitä kutsutaan kun halutaan lajitteluarvo
205     */
206    public OnGridCell<TYPE> getOnCellValue() {
207        return onCellValue;
208    }
209
210    
211    /**
212     *  @param onCellValue mitä kutsutaan kun halutaan näytettävä arvo
213     */
214    public void setOnCellValue(OnGridCell<TYPE> onCellValue) {
215        this.onCellValue = onCellValue;
216    }
217
218
219    /**
220     * @return mitä kutsutaan kun halutaan solun sisältöä merkkijonona
221     */
222    public OnGridCell<TYPE> getOnCellString() {
223        return onCellString;
224    }
225
226    
227    /**
228     *  @param onCellString mitä kutsutaan kun halutaan lajitteluarvo
229     */
230    public void setOnCellString(OnGridCell<TYPE> onCellString) {
231        this.onCellString = onCellString;
232    }
233
234
235    /**
236     * @return muokkauksen käsittelijä
237     */
238    public OnGridCell<TYPE> getOnGridEdit() {
239        return onGridEdit;
240    }
241
242
243    /**
244     * @param onGridEdit uusi käsittelijä muokkaukselle
245     */
246    public void setOnGridEdit(OnGridCell<TYPE> onGridEdit) {
247        this.onGridEdit = onGridEdit;
248    }
249
250
251    /**
252     * @return muokkauksen käsittelijä
253     */
254    public OnGridLiveEdit<TYPE> getOnGridLiveEdit() {
255        return onGridLiveEdit;
256    }
257
258
259    /**
260     * @param onGridLiveEdit uusi käsittelijä muokkaukselle
261     */
262    public void setOnGridLiveEdit(OnGridLiveEdit<TYPE> onGridLiveEdit) {
263        this.onGridLiveEdit = onGridLiveEdit;
264    }
265
266
267    /**
268     * Initializes the control.
269     */
270    @FXML
271    public void initialize() {
272        itemsProperty().bind(rivitProp);
273        rivitProp.set(tableRows);
274    }
275
276    
277    /**
278     * Asetetaan taulukon sisältö.
279     * @param data monirivinen lista, jossa 1. rivi on otsikot ja muut dataa. Alkiot eroteltu |-merkillä.
280     */
281    public void setRivit(String data) {
282        this.rivitJono.set(data);
283        String[] rows = data.split("\n");
284        this.getColumns().clear();
285        tableRows.clear();
286        if ( rows.length == 0 ) return;
287        String[] headings = rows[0].split("\\|");
288        initTable(headings);
289        for (int i=1; i<rows.length; i++) {
290            add(null, rows[i].split("\\|"));
291            // rivit.add(new GridItem<TYPE>(tiedot[i].split("\\|"),null, i-1));
292        }
293    }
294
295
296    /**
297     * Lisätään taulukkoon otsikot-mukaisesti sarakkeet ja 
298     * @param headings sarakkaiden määrä ja otsikot tästä
299     */
300    public void initTable(String... headings) {
301        tableRows.clear();
302        alignments.clear();
303        getColumns().clear();
304        for (int i = 0; i < headings.length; i++) {
305            TableColumn<GridRowItem<TYPE>, String> tc = new TableColumn<GridRowItem<TYPE>, String>(headings[i]);
306            final int origCol = i;
307            
308            tc.setCellValueFactory((rivi) -> { 
309                GridRowItem<TYPE> tableRow = rivi.getValue();
310                String s = get(rivi.getValue().getRowNr(),origCol);
311                if ( onCellString != null ) s = onCellString.onGetCell(this, tableRow.getItem(), s, tableRow.getRowNr(), origCol);
312                if ( onCellValue == null )
313                    return new SimpleStringProperty(s.replace(" ", "."));
314                //  String onEdit(StringGrid<TYPE> grid, TYPE item, String newValue, int row, int column);
315                String val = this.onCellValue.onGetCell(this, tableRow.getItem(), s, tableRow.getRowNr(), origCol);
316                if ( val == null ) val = s;
317                return new SimpleStringProperty(val.replace(" ", "."));
318             });
319            
320            tc.setPrefWidth(90);
321            tc.setId(""+i);
322            getColumns().add(tc);
323            // tc.setCellFactory(TextFieldTableCell.forTableColumn());
324            tc.setCellFactory(column -> new StringGridCell<TYPE>(origCol)); 
325            tc.setOnEditCommit( (t) -> { 
326                StringGrid<TYPE> table = (StringGrid<TYPE>)t.getTableView();
327                GridRowItem<TYPE> tableRow = table.getItems().get(t.getTablePosition().getRow());
328                String s = t.getNewValue();
329                if ( s == null ) return;
330                int r = table.findRowNr(tableRow);
331                if ( table.onGridEdit != null ) s = table.onGridEdit.onGetCell(table,tableRow.getItem(), s, r, origCol);
332                if ( s == null ) return;
333                tableRow.set(origCol, s);
334                table.refresh();
335                }
336            );
337        }
338    }
339    
340
341    /**
342     * Lisätään uusi alkio taulukkoon
343     * @param obj mihin objektiin viitataan
344     * @param items lisättävät merkkijonot
345     */
346    public void add(TYPE obj, String...items) {
347        tableRows.add(new GridRowItem<TYPE>(items,obj, tableRows.size()));
348        refresh();
349    }
350    
351    
352    /**
353     * Lisätään uusi alkio taulukkoon.  Jotta tämä toimisi, pitää olla
354     * tehtynä vähintään set setOnCellString
355     * @param obj mihin objektiin viitataan
356     */
357    public void add(TYPE obj) {
358        add(obj,new String[0]);
359    }
360    
361    
362    /**
363     * Lisätään uudet jonot taulukkoon
364     * @param items lisättävät merkkijonot
365     */
366    public void add(String...items) {
367        add(null,items);
368    }
369    
370    
371    /**
372     * Lisätään uudet alkiot taulukkoon.  Jotta tämä toimisi, pitää olla
373     * tehtynä vähintään set setOnCellString
374     * @param objs mitkä oliot lisätään
375     */
376    public void add(Collection<TYPE> objs) {
377        for (TYPE obj: objs) add(obj);
378    }
379    
380    
381    /**
382     * Lisätään uudet alkiot taulukkoon.  Jotta tämä toimisi, pitää olla
383     * tehtynä vähintään set setOnCellString
384     * @param objs mitkä oliot lisätään
385     */
386    @SuppressWarnings("unchecked")
387    public void add(TYPE... objs) {
388        for (TYPE obj: objs) add(obj);
389    }
390    
391    
392    /**
393     * Poistaa kaikki rivit;
394     */
395    public void clear() {
396        tableRows.clear();
397        refresh();
398    }
399
400
401    /**
402     * @return rivijonon sisältö.  
403     */
404    public String getRivit() {
405        // return rivitJono.get();
406        StringBuilder tulos = new StringBuilder();
407        String erotin = "";
408        for (TableColumn<GridRowItem<TYPE>, ?> tc : getColumns() ) {
409            tulos.append(erotin + tc.getText());
410            erotin = "|";
411        }
412        tulos.append("\n");
413        int len = tableRows.size();
414        for (int i=0; i<len; i++) {
415            GridRowItem<TYPE> rivi = findTableRow(i);
416            if ( rivi == null ) continue;
417            erotin = "";
418            for (String s: rivi.getRow()) {
419                tulos.append(erotin + s);
420                erotin = "|";
421            }
422            tulos.append("\n");
423        }
424        return tulos.toString();
425    }
426
427    
428    /**
429     * Asettaa sarakkeet lajiteltavaksi
430     * @param col mikä sarake, -1 on kaikki
431     * @param sortable lajiteltava vai ei
432     */
433    public void setSortable(int col, boolean sortable) {
434        for (TableColumn<GridRowItem<TYPE>, ?> tc : getColumns() ) {
435            if ( col == -1 || tc.getId().equals(""+col))
436                tc.setSortable(sortable);
437        }
438    }
439    
440    
441    /**
442     * Asetetaan valitulle sarakkeelle numeerinen järjestely
443     * @param col mille sarakkeelle;
444     */
445    public void setColumnSortOrderNumber(int col) {
446        if ( col < 0 || col >= getColumns().size() ) return;
447        @SuppressWarnings("unchecked")
448        TableColumn<GridRowItem<TYPE>, String> tc = (TableColumn<GridRowItem<TYPE>, String>) getColumns().get(col); 
449        tc.setCellValueFactory((rivi) -> {
450            int r = rivi.getValue().getRowNr();
451            String s = get(r,col);
452            s = "00000000000"+s;
453            s = s.substring(s.length()-10, s.length());
454            return new SimpleStringProperty(s);
455        });
456        alignments.put(col, Pos.CENTER_RIGHT);
457    }
458    
459    
460    /**
461     * Asettaa sarakkeen leveyden
462     * @param col mikä sarake, -1 on kaikki
463     * @param width mikä on leveys
464     */
465    public void setColumnWidth(int col, double width) {
466        for (TableColumn<GridRowItem<TYPE>, ?> tc : getColumns() ) {
467            if ( col == -1 || tc.getId().equals(""+col)) {
468                tc.setPrefWidth(width);
469                tc.setMaxWidth(width);
470                tc.setMinWidth(width);
471            }
472        }
473    }
474    
475    
476    /**
477     * Sarakkeen solujen sijoitus
478     * @param col mistä sarakkeesta
479     * @return mihin reunaan solut sijoitetaan
480     */
481    public Pos getAlignment(int col) {
482        Pos result = alignments.get(col);
483        if ( result == null ) return Pos.CENTER_LEFT;
484        return result;
485    }
486    
487    
488    /**
489     * Sarakkeen solujen sijoitus
490     * @param col minkä sarakkeen sijoitus
491     * @param align mihin keskitetään, esim. Pos.CENTER_CENTER
492     */
493    public void setAlignment(int col, Pos align) {
494        alignments.put(col, align);
495    }
496
497    
498    /**
499     * Sarakkeen solujen sijoitus Swing-vakioiden avulla
500     * @param col minkä sarakkeen sijoitus
501     * @param align mihin keskitetään, esim. SwingConstants.RIGHT
502     */
503    public void setAlignment(int col, int align) {
504        switch (align) {
505        case SwingConstants.RIGHT:
506           setAlignment(col, Pos.CENTER_RIGHT);
507           break;
508        case SwingConstants.CENTER:
509            setAlignment(col, Pos.CENTER);
510            break;
511        default:
512            setAlignment(col, Pos.CENTER_LEFT);
513        }
514    }
515
516    
517    /**
518     * @return jonon sisältö ominaisuutena
519     */
520    public StringProperty getRivitProperty() {
521        return rivitJono;
522    }
523    
524    
525    /**
526     * Pakotetaan luomaan cellit uudelleen
527    public void forceRefresh() {
528        getProperties().put(TableViewSkinBase.RECREATE, Boolean.TRUE);
529    }
530    */
531
532    
533    /**
534     * @param tableRow mitä riviä etsitään 
535     * @return rivin alkuperäinen rivi-indeksi
536     */
537    protected int findRowNr(GridRowItem<TYPE> tableRow) {
538        if ( tableRow == null ) return -1;
539        return tableRow.getRowNr();
540    }
541    
542
543    /**
544     * Valitaan taulukosta tietty rivi. Mikäli rivi liian iso, valitaan viimeinen, mikäli
545     * liian pieni, valitaan ensimmäinen (indeksi 0);
546     * @param rowvisible mikä rivi näkyvissä olevalla järjestyksellä 
547     */
548    public void selectRow(int rowvisible) {
549        int newrow = rowvisible;
550        int rowcount = getItems().size(); 
551        if ( rowvisible >= rowcount ) newrow = rowcount -1;
552        getFocusModel().focus(newrow);
553        getSelectionModel().select(newrow);
554    }
555
556    
557    /**
558     * @return alkuperäinen rivinumero valitulle riville
559     */
560    public int getRowNr() {
561        // int r = getSelectionModel().getSelectedIndex();
562        GridRowItem<TYPE> rivi = getSelectionModel().getSelectedItem();
563        return findRowNr(rivi);
564    }
565    
566    
567    /**
568     * @return valittu sarake alkuperäisissä koordinaateissa
569     */
570    public int getColumnNr() {
571        @SuppressWarnings("unchecked")
572        TableColumn<GridRowItem<TYPE>, String> tc = getFocusModel().getFocusedCell().getTableColumn();
573        if ( tc == null ) return -1;
574        return Integer.parseInt(tc.getId());
575    }
576    
577    
578    /**
579     * Etsitään rivi, jolla pyydetty indeksi
580     * @param row mikä rivi etsitään
581     * @return rivi tai null jos ei löydy
582     */
583    private GridRowItem<TYPE> findTableRow(int row) {
584        for (GridRowItem<TYPE> r: tableRows)
585            if ( r.getRowNr() == row ) return r;
586        return null;
587    }
588    
589    
590    /**
591     * Asetetaan solun arvo
592     * @param s uusi arvo solulle
593     * @param row rivi
594     * @param col sarake
595     */
596    public void set(String s, int row, int col) {
597        GridRowItem<TYPE> rivi = findTableRow(row);
598        if ( rivi == null ) return;
599        rivi.set(col,s);
600        refresh(); 
601    }
602    
603    
604    /**
605     * Palautetaan solun arvo
606     * @param row miltä riviltä alkuperäisillä indekseillä
607     * @param col mistä sarakkeesta
608     * @return solun arvo, "" jos väärät indeksit
609     */
610    public String get(int row, int col) {
611        GridRowItem<TYPE> rivi = findTableRow(row);
612        if ( rivi == null ) return "";
613        String s = rivi.get(col);
614        if ( onCellString == null ) return s;
615        String val = onCellString.onGetCell(this, rivi.getItem(), s, row, col);
616        if ( val == null ) val = s;
617        if ( val == null ) val = "";
618        return val;    
619    }
620    
621    
622    /**
623     * Asetetaan solun arvo
624     * @param obj mitä olioita rivi edustaa
625     * @param row rivi
626     */
627    public void setObject(TYPE obj, int row) {
628        GridRowItem<TYPE> rivi = findTableRow(row);
629        if ( rivi == null ) return;
630        rivi.setItem(obj);
631        refresh(); 
632    }
633    
634    
635    /**
636     * Palautetaan riviä vastaava olio
637     * @param row miltä riviltä alkuperäisillä indekseillä
638     * @return riviä vastaava olio, null jos väärät indeksit
639     */
640    public TYPE getObject(int row) {
641        GridRowItem<TYPE> rivi = findTableRow(row);
642        if ( rivi == null ) return null;
643        return rivi.getItem();
644    }
645    
646    
647    /**
648     * Palautetaan valittua riviä vastaava olio
649     * @return riviä vastaava olio, null jos ei valittua
650     */
651    public TYPE getObject() {
652        GridRowItem<TYPE> rivi = getSelectionModel().getSelectedItem();
653        if ( rivi == null ) return null;
654        return rivi.getItem();
655    }
656    
657    
658    /**
659     * Asetetaan solun uusi tyyli 
660     * @param s uusi tyylinimi solulle.  Voi olla montakin pilkulla tai välilyönnille eroteltua nimeä.
661     * @param row rivi
662     * @param col sarake
663     */
664    public void setStyleClass(String s, int row, int col) {
665        GridRowItem<TYPE> rivi = findTableRow(row);
666        if ( rivi == null ) return;
667        rivi.setCellClass(s, col);
668        refresh(); 
669    }
670    
671    
672    /**
673     * Solun asetetut tyylit 
674     * @param row rivi
675     * @param col sarake
676     * @return null jos ei tyyliä, muuten tyylin nimi tai nimet kuten annettu
677     */
678    public String getStyleClass(int row, int col) {
679        GridRowItem<TYPE> rivi = findTableRow(row);
680        if ( rivi == null ) return null;
681        return rivi.getCellClass(col);
682    }
683    
684
685    /**
686     * Lisätään styles listaan jonosta luokat jotka erotettu pilkuilla tai välilyönneillä
687     * @param styles mihin listaan lisätään
688     * @param newClasses mitä tyylejä lisätään
689     */
690    public static void addStyleClasses(ObservableList<String> styles, String newClasses) {
691        if ( newClasses == null ) return;
692        for (String sc: newClasses.split("[, ]"))
693            if ( !sc.isEmpty() && !styles.contains(sc) )
694                styles.add(sc);
695    }
696    
697    
698    /**
699     * @return mikä luokka tyhjille soluille joissa ei ole omaa dataa
700     */
701    public String getEmptyStyleClass() {
702        return emptyStyleClass;
703    }
704
705 
706    /**
707     * @param emptyStyleClass tyhjien solujen luokka (tai luokat eroteltuina pilkulla tai välilöynnillä)
708     */
709    public void setEmptyStyleClass(String emptyStyleClass) {
710        this.emptyStyleClass = emptyStyleClass;
711        refresh();
712    }
713
714    
715    /**
716     * Estetään sarakkeiden järjestäminen.
717     * Surkea häck, kopioitu 
718     * http://stackoverflow.com/questions/10598639/how-to-disable-column-reordering-in-a-javafx2-tableview
719     * Eli jos järjestys muuttuu, palautetaan se heti.
720     */
721    public void disableColumnReOrder() {
722        final ArrayList<TableColumn<GridRowItem<TYPE>,?>> columns = new ArrayList<>();
723        columns.addAll(getColumns());
724        getColumns().addListener(new ListChangeListener<Object>() {
725            public boolean suspended;
726
727            @Override
728            public void onChanged(Change<?> change) {
729                change.next();
730                if (change.wasReplaced() && !suspended) {
731                    this.suspended = true;
732                    getColumns().setAll(columns);
733                    this.suspended = false;
734                }
735            }
736        });        
737    }
738    
739    
740    /**
741     * Luokka yhdelle solulle
742     * @param <TYPE> minkä tyyppisiin alkioihin viitataan
743     */
744    protected static class StringGridCell<TYPE> extends TableCell<GridRowItem<TYPE>, String> {
745        
746        private TextField textField;
747        private int origCol;
748
749        /**
750         * @param col mitä saraketta solu vastaa
751         */
752        public StringGridCell(int col) {
753            origCol = col;
754        }
755        
756        
757        /**
758         * @return taulukko, johon kuulutaan
759         */
760        public StringGrid<TYPE> getTable() {
761           return (StringGrid<TYPE>)getTableView(); 
762        }
763        
764        
765        /**
766         * @return antaa kohdalla olevan tietueen
767         */
768        protected GridRowItem<TYPE> getRowItem() {
769            @SuppressWarnings("unchecked")
770            TableRow<GridRowItem<TYPE>> row = getTableRow();
771            if ( row == null ) return null;
772            return row.getItem();
773        }
774
775        
776        /**
777         * @return antaa kohdalla olevan tietueen kentän sisällön merkkijonona
778         */
779        protected String getCellString() {
780            GridRowItem<TYPE> tableRow = getRowItem();
781            if ( tableRow == null ) return getItem();
782            StringGrid<TYPE> table = getTable();
783            return table.get(tableRow.getRowNr(), origCol);
784        }
785
786        
787        @Override
788        public void startEdit() {
789            if ( isEmpty()) return;
790            super.startEdit();
791            createTextField();
792            setText(null);
793            setGraphic(textField);
794            textField.setText(getCellString());
795            Platform.runLater(() -> textField.requestFocus());
796            textField.selectAll();
797        }
798
799
800        @Override
801        public void cancelEdit() {
802            super.cancelEdit();
803            setText(getCellString());
804            setGraphic(null);
805        }
806
807
808        @Override
809        protected void updateItem(String s, boolean empty) {
810            setAlignment(getTable().getAlignment(origCol));
811
812            super.updateItem(getCellString(), empty);
813            if (isEditing()) {
814                if (textField != null) textField.setText(getCellString());
815                setText(null);
816                setGraphic(textField);
817                return;
818            }              
819            
820            if ( empty ) {
821                addStyleClasses(getStyleClass(),getTable().getEmptyStyleClass());
822                return;
823            }
824
825            @SuppressWarnings("unchecked")
826            TableRow<GridRowItem<TYPE>> row = getTableRow();
827            if ( row != null && row.getItem() != null) {
828                String styleClass = row.getItem().getCellClass(origCol);
829                addStyleClasses(getStyleClass(),styleClass);
830                setText(getCellString());
831            }
832            // setStyle("-fx-background-color: yellow;-fx-text-fill:blue");
833        }    
834
835        @SuppressWarnings("unchecked")
836        private void createTextField() {
837            if ( textField != null ) return;
838            textField = new TextField();
839            textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
840            textField.focusedProperty().addListener( (arg0, arg1,arg2) -> {
841                if (!arg2) {
842                    commitEdit(textField.getText());
843                }
844            });
845            textField.setOnAction(e -> commitEdit(textField.getText()));
846            textField.setOnKeyReleased(e -> {
847                if ( e == null ) return;
848                if ( textField == null ) return;
849                GridRowItem<TYPE> tietue = getRowItem();
850                String s = textField.getText();
851                StringGrid<TYPE> table = getTable();
852                if ( table.getOnGridLiveEdit() != null ) {
853                    int r = table.findRowNr(tietue);
854                    s = table.getOnGridLiveEdit().onEdit(table, tietue.getItem(), s, r, origCol, textField);
855                }
856                if ( s != null ) tietue.set(origCol, s);
857            });
858            textField.setOnKeyPressed(t -> {
859                if (t.getCode() == KeyCode.ENTER) {
860                    commitEdit(textField.getText());
861                    cancelEdit();
862                    t.consume();
863                } else if (t.getCode() == KeyCode.ESCAPE) {
864                    cancelEdit();
865                    t.consume();
866                } else if (t.getCode() == KeyCode.TAB) {
867                    // commitEdit(textField.getText()); // ei tarvii kun tallennetetaan muutenkin
868                    cancelEdit();
869                    t.consume();
870                    StringGrid<TYPE> table = getTable();
871                    if ( t.isShiftDown() )
872                        table.getFocusModel().focusLeftCell();
873                    else 
874                        table.getFocusModel().focusRightCell();
875                    
876                    table.edit(getTableRow().getIndex(), table.getFocusModel().getFocusedCell().getTableColumn());
877                }
878            });
879        }
880    
881    }
882    
883}