OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.view.list;
15
 
19 ilm 16
import org.openconcerto.openoffice.XMLFormatVersion;
17 ilm 17
import org.openconcerto.openoffice.spreadsheet.SpreadSheet;
18
import org.openconcerto.sql.Log;
73 ilm 19
import org.openconcerto.sql.TM;
80 ilm 20
import org.openconcerto.sql.element.SQLComponent;
17 ilm 21
import org.openconcerto.sql.element.SQLElement;
22
import org.openconcerto.sql.element.SQLElementDirectory;
23
import org.openconcerto.sql.model.SQLField;
24
import org.openconcerto.sql.model.SQLRow;
25
import org.openconcerto.sql.model.SQLRowAccessor;
26
import org.openconcerto.sql.model.SQLRowValues;
27
import org.openconcerto.sql.model.SQLTable;
28
import org.openconcerto.sql.model.Where;
29
import org.openconcerto.sql.request.ListSQLRequest;
80 ilm 30
import org.openconcerto.sql.request.UpdateBuilder;
31
import org.openconcerto.sql.users.User;
32
import org.openconcerto.sql.users.UserManager;
174 ilm 33
import org.openconcerto.sql.users.rights.TableAllRights;
19 ilm 34
import org.openconcerto.sql.view.FileTransfertHandler;
17 ilm 35
import org.openconcerto.sql.view.IListener;
21 ilm 36
import org.openconcerto.sql.view.list.IListeAction.ButtonsBuilder;
37
import org.openconcerto.sql.view.list.IListeAction.IListeEvent;
38
import org.openconcerto.sql.view.list.IListeAction.PopupBuilder;
39
import org.openconcerto.sql.view.list.IListeAction.PopupEvent;
40
import org.openconcerto.sql.view.list.RowAction.PredicateRowAction;
17 ilm 41
import org.openconcerto.ui.FontUtils;
42
import org.openconcerto.ui.FormatEditor;
19 ilm 43
import org.openconcerto.ui.MenuUtils;
21 ilm 44
import org.openconcerto.ui.PopupMouseListener;
19 ilm 45
import org.openconcerto.ui.SwingThreadUtils;
80 ilm 46
import org.openconcerto.ui.list.selection.BaseListStateModel;
17 ilm 47
import org.openconcerto.ui.list.selection.ListSelection;
48
import org.openconcerto.ui.list.selection.ListSelectionState;
49
import org.openconcerto.ui.state.JTableStateManager;
50
import org.openconcerto.ui.table.AlternateTableCellRenderer;
51
import org.openconcerto.ui.table.ColumnSizeAdjustor;
52
import org.openconcerto.ui.table.TableColumnModelAdapter;
53
import org.openconcerto.ui.table.TablePopupMouseListener;
54
import org.openconcerto.ui.table.ViewTableModel;
93 ilm 55
import org.openconcerto.ui.table.XTableColumnModel;
17 ilm 56
import org.openconcerto.utils.CollectionUtils;
57
import org.openconcerto.utils.CompareUtils;
58
import org.openconcerto.utils.FormatGroup;
93 ilm 59
import org.openconcerto.utils.StringUtils;
17 ilm 60
import org.openconcerto.utils.TableModelSelectionAdapter;
61
import org.openconcerto.utils.TableSorter;
62
import org.openconcerto.utils.Tuple2;
21 ilm 63
import org.openconcerto.utils.cc.IPredicate;
17 ilm 64
import org.openconcerto.utils.cc.ITransformer;
65
import org.openconcerto.utils.convertor.StringClobConvertor;
28 ilm 66
import org.openconcerto.utils.text.BooleanFormat;
17 ilm 67
 
25 ilm 68
import java.awt.Color;
17 ilm 69
import java.awt.Component;
19 ilm 70
import java.awt.FlowLayout;
71
import java.awt.Font;
17 ilm 72
import java.awt.GridBagConstraints;
73
import java.awt.GridBagLayout;
74
import java.awt.Insets;
75
import java.awt.event.ActionEvent;
76
import java.awt.event.HierarchyEvent;
77
import java.awt.event.HierarchyListener;
78
import java.awt.event.KeyAdapter;
79
import java.awt.event.KeyEvent;
80
import java.awt.event.MouseAdapter;
81
import java.awt.event.MouseEvent;
82
import java.beans.PropertyChangeEvent;
83
import java.beans.PropertyChangeListener;
84
import java.beans.PropertyChangeListenerProxy;
85
import java.beans.PropertyChangeSupport;
86
import java.io.File;
87
import java.io.IOException;
88
import java.sql.Clob;
20 ilm 89
import java.sql.Time;
28 ilm 90
import java.sql.Timestamp;
17 ilm 91
import java.text.DateFormat;
28 ilm 92
import java.text.Format;
17 ilm 93
import java.util.ArrayList;
94
import java.util.Calendar;
95
import java.util.Collection;
96
import java.util.Collections;
97
import java.util.Date;
21 ilm 98
import java.util.EventObject;
17 ilm 99
import java.util.HashMap;
93 ilm 100
import java.util.HashSet;
19 ilm 101
import java.util.LinkedHashMap;
17 ilm 102
import java.util.List;
28 ilm 103
import java.util.Locale;
17 ilm 104
import java.util.Map;
21 ilm 105
import java.util.Map.Entry;
93 ilm 106
import java.util.Set;
17 ilm 107
import java.util.concurrent.ExecutionException;
108
 
109
import javax.swing.AbstractAction;
110
import javax.swing.Action;
80 ilm 111
import javax.swing.DropMode;
17 ilm 112
import javax.swing.InputMap;
19 ilm 113
import javax.swing.JButton;
17 ilm 114
import javax.swing.JCheckBoxMenuItem;
115
import javax.swing.JComponent;
19 ilm 116
import javax.swing.JMenuItem;
17 ilm 117
import javax.swing.JPanel;
118
import javax.swing.JPopupMenu;
119
import javax.swing.JScrollPane;
120
import javax.swing.JTable;
121
import javax.swing.JTextField;
122
import javax.swing.KeyStroke;
19 ilm 123
import javax.swing.ListSelectionModel;
17 ilm 124
import javax.swing.SwingUtilities;
125
import javax.swing.SwingWorker;
126
import javax.swing.event.AncestorEvent;
127
import javax.swing.event.AncestorListener;
128
import javax.swing.event.ListSelectionEvent;
129
import javax.swing.event.ListSelectionListener;
130
import javax.swing.event.TableColumnModelEvent;
80 ilm 131
import javax.swing.event.TableModelEvent;
17 ilm 132
import javax.swing.event.TableModelListener;
133
import javax.swing.table.DefaultTableCellRenderer;
134
import javax.swing.table.TableCellRenderer;
135
import javax.swing.table.TableColumn;
136
import javax.swing.table.TableColumnModel;
137
import javax.swing.table.TableModel;
138
 
142 ilm 139
import net.jcip.annotations.GuardedBy;
140
 
17 ilm 141
/**
132 ilm 142
 * Une liste de lignes correspondant à une ListSQLRequest. Diagramme pour la sélection :
143
 * <img src="doc-files/listSelection.png"/><br/>
17 ilm 144
 *
145
 * @author ILM Informatique
146
 */
63 ilm 147
public final class IListe extends JPanel {
17 ilm 148
 
80 ilm 149
    static private final class LockAction extends RowAction {
174 ilm 150
        private final boolean lock;
151
 
80 ilm 152
        public LockAction(final boolean lock) {
153
            super(new AbstractAction(TM.tr(lock ? "ilist.lockRows" : "ilist.unlockRows")) {
154
                @Override
155
                public void actionPerformed(ActionEvent e) {
156
                    final IListe list = IListe.get(e);
157
                    final List<Integer> ids = list.getSelection().getSelectedIDs();
158
                    final SQLTable t = list.getSource().getPrimaryTable();
159
                    final UpdateBuilder update = new UpdateBuilder(t);
160
                    update.setObject(SQLComponent.READ_ONLY_FIELD, lock ? SQLComponent.READ_ONLY_VALUE : SQLComponent.READ_WRITE_VALUE);
161
                    final User user = UserManager.getUser();
162
                    if (user != null)
163
                        update.setObject(SQLComponent.READ_ONLY_USER_FIELD, user.getId());
164
                    update.setWhere(new Where(t.getKey(), ids));
165
                    t.getDBSystemRoot().getDataSource().execute(update.asString());
166
                    // don't fire too many times, as each one will cause UpdateQueue to issue a
167
                    // request
168
                    final Collection<? extends Number> fireIDs = ids.size() < 12 ? ids : Collections.singleton(SQLRow.NONEXISTANT_ID);
169
                    for (final Number fireID : fireIDs)
170
                        t.fireTableModified(fireID.intValue(), update.getFieldsNames());
171
                }
172
            }, false, true);
174 ilm 173
            this.lock = lock;
80 ilm 174
        }
175
 
176
        @Override
177
        public boolean enabledFor(IListeEvent evt) {
174 ilm 178
            boolean hasRight = TableAllRights.currentUserHasRight(this.lock ? TableAllRights.USER_UI_LOCK_ROW : TableAllRights.USER_UI_UNLOCK_ROW, evt.getTable());
179
            return !evt.getSelectedRows().isEmpty() && hasRight;
80 ilm 180
        }
181
    }
182
 
183
    private static LockAction LOCK_ACTION;
184
    private static LockAction UNLOCK_ACTION;
185
 
186
    private static final LockAction getLockAction() {
187
        assert SwingUtilities.isEventDispatchThread();
188
        // don't create too early as we might not have the localisation available. Further some
189
        // applications will never use it.
190
        if (LOCK_ACTION == null)
191
            LOCK_ACTION = new LockAction(true);
192
        return LOCK_ACTION;
193
    }
194
 
195
    private static final LockAction getUnlockAction() {
196
        assert SwingUtilities.isEventDispatchThread();
197
        if (UNLOCK_ACTION == null)
198
            UNLOCK_ACTION = new LockAction(false);
199
        return UNLOCK_ACTION;
200
    }
201
 
65 ilm 202
    /**
203
     * When this system property is set, table {@link JTableStateManager state} is never read nor
204
     * written. I.e. the user can change the table state but it will be reset at each launch.
205
     */
206
    public static final String STATELESS_TABLE_PROP = "org.openconcerto.sql.list.statelessTable";
80 ilm 207
    private static final String SELECTION_DATA_PROPNAME = "selectionData";
65 ilm 208
 
28 ilm 209
    static private final class FormatRenderer extends DefaultTableCellRenderer {
210
        private final Format fmt;
211
 
212
        private FormatRenderer(Format fmt) {
213
            super();
214
            this.fmt = fmt;
215
        }
216
 
217
        @Override
218
        protected void setValue(Object value) {
219
            this.setText(value == null ? "" : this.fmt.format(value));
220
        }
221
    }
222
 
17 ilm 223
    private static boolean FORCE_ALT_CELL_RENDERER = false;
224
    static final String SEP = " ► ";
28 ilm 225
 
80 ilm 226
    // DefaultTableCellRenderer is stateful, so safer to not share (JTable also has private
227
    // instances, see createDefaultRenderers())
228
    public static final TableCellRenderer createDateRenderer() {
229
        return new FormatRenderer(DateFormat.getDateInstance(DateFormat.MEDIUM));
230
    }
231
 
232
    public static final TableCellRenderer createTimeRenderer() {
233
        return new FormatRenderer(DateFormat.getTimeInstance(DateFormat.SHORT));
234
    }
235
 
236
    public static final TableCellRenderer createDateTimeRenderer() {
237
        return new FormatRenderer(DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT));
238
    }
239
 
17 ilm 240
    private static final Map<Class<?>, FormatGroup> FORMATS;
241
    static {
242
        FORMATS = new HashMap<Class<?>, FormatGroup>();
243
        FORMATS.put(Date.class, new FormatGroup(DateFormat.getDateInstance(DateFormat.SHORT), DateFormat.getDateInstance(DateFormat.MEDIUM), DateFormat.getDateInstance(DateFormat.LONG)));
20 ilm 244
        // longer first otherwise seconds are not displayed by the cell editor and will be lost
245
        FORMATS.put(Time.class, new FormatGroup(DateFormat.getTimeInstance(DateFormat.MEDIUM), DateFormat.getTimeInstance(DateFormat.SHORT)));
28 ilm 246
        FORMATS.put(Timestamp.class, new FormatGroup(DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM), DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT),
247
                DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM), DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT)));
65 ilm 248
 
17 ilm 249
    }
250
 
251
    public static final void remove(InputMap m, KeyStroke key) {
252
        InputMap current = m;
253
        while (current != null) {
254
            current.remove(key);
255
            current = current.getParent();
256
        }
257
    }
258
 
259
    /**
260
     * Whether to force table cell renderers to always be alternate. I.e. even after the list
261
     * creation, if the renderer of a cell is changed, a listener will wrap it in an
262
     * {@link AlternateTableCellRenderer} if necessary.
263
     *
264
     * @param force <code>true</code> to listen to renderer change, and wrap it in an
265
     *        {@link AlternateTableCellRenderer}.
266
     */
267
    public static void setForceAlternateCellRenderer(boolean force) {
268
        FORCE_ALT_CELL_RENDERER = force;
269
    }
270
 
21 ilm 271
    public static final IListe get(EventObject evt) {
19 ilm 272
        return SwingThreadUtils.getAncestorOrSelf(IListe.class, (Component) evt.getSource());
273
    }
274
 
17 ilm 275
    // *** instance
276
 
277
    private final JTable jTable;
278
    private final JTextField filter;
279
    private boolean debugFilter;
142 ilm 280
    @GuardedBy("this")
17 ilm 281
    private FilterWorker filterWorker;
282
    // optional popup on the table
283
    private final JPopupMenu popup;
284
    private final TableSorter sorter;
142 ilm 285
    @GuardedBy("this")
286
    // record the source when non-displayable (ie getModel() == null), also allow to be read outside
287
    // of the EDT
17 ilm 288
    private SQLTableModelSource src;
289
    private boolean adjustVisible;
290
    private ColumnSizeAdjustor tcsa;
291
 
21 ilm 292
    private final Map<IListeAction, ButtonsBuilder> rowActions;
19 ilm 293
    // double-click
21 ilm 294
    private IListeAction defaultRowAction;
19 ilm 295
    private final JPanel btnPanel;
28 ilm 296
    private final Map<Class<?>, FormatGroup> searchFormats;
19 ilm 297
 
17 ilm 298
    // * selection
299
    private final List<IListener> listeners;
300
    private final List<IListener> naListeners;
301
 
302
    // * listeners
303
    private final PropertyChangeSupport supp;
304
    // for not adjusting listeners
305
    private final ListSelectionListener selectionListener;
80 ilm 306
    private final TableModelListener selectionDataListener;
17 ilm 307
    // filter
308
    private final PropertyChangeListener filterListener;
309
    // listen on model's properties
310
    private final List<PropertyChangeListener> modelPCListeners;
311
 
312
    private final ListSelectionState state;
313
    private final JTableStateManager tableStateManager;
314
 
21 ilm 315
    private int retainCount = 0;
316
 
151 ilm 317
    private boolean cellModificationAllowed = ITableModel.isDefaultCellsEditable();
318
    private boolean orderModificationAllowed = ITableModel.isDefaultOrderEditable();
319
 
17 ilm 320
    public IListe(final SQLTableModelSource req) {
321
        this(req, null);
322
    }
323
 
142 ilm 324
    public IListe(final SQLTableModelSource req, final File configFile) {
17 ilm 325
        if (req == null)
326
            throw new NullPointerException("Création d'une IListe avec une requete null");
327
 
21 ilm 328
        this.rowActions = new LinkedHashMap<IListeAction, ButtonsBuilder>();
17 ilm 329
        this.supp = new PropertyChangeSupport(this);
330
        this.listeners = new ArrayList<IListener>();
331
        this.naListeners = new ArrayList<IListener>();
332
        this.modelPCListeners = new ArrayList<PropertyChangeListener>();
333
 
334
        this.sorter = new TableSorter();
335
        this.jTable = new JTable(this.sorter) {
336
            @Override
337
            public String getToolTipText(MouseEvent event) {
338
                final String original = super.getToolTipText(event);
339
 
340
                // Locate the row under the event location
341
                final int rowIndex = rowAtPoint(event.getPoint());
342
                // has already happened on M3 (not sure how)
343
                if (rowIndex < 0)
344
                    return original;
345
 
346
                final List<String> infoL = new ArrayList<String>();
347
                if (original != null) {
348
                    final String html = "<html>";
349
                    if (original.startsWith(html))
350
                        // -1 since the closing tag is </html>
351
                        infoL.add(original.substring(html.length(), original.length() - html.length() - 1));
352
                    else
353
                        infoL.add(original);
354
                }
355
 
19 ilm 356
                final SQLRowValues row = ITableModel.getLine(this.getModel(), rowIndex).getRow();
17 ilm 357
 
73 ilm 358
                final String create = getLine(true, row, getSource().getPrimaryTable().getCreationUserField(), getSource().getPrimaryTable().getCreationDateField());
17 ilm 359
                if (create != null)
360
                    infoL.add(create);
73 ilm 361
                final String modif = getLine(false, row, getSource().getPrimaryTable().getModifUserField(), getSource().getPrimaryTable().getModifDateField());
17 ilm 362
                if (modif != null)
363
                    infoL.add(modif);
80 ilm 364
                // TODO locked by
17 ilm 365
 
366
                final String info;
367
                if (infoL.size() == 0)
368
                    info = null;
369
                else
370
                    info = "<html>" + CollectionUtils.join(infoL, "<br/>") + "</html>";
371
                // ATTN doesn't follow the mouse if info remains the same, MAYBE add an identifier
372
                return info;
373
            }
374
 
73 ilm 375
            public String getLine(final boolean created, final SQLRowValues row, final SQLField userF, final SQLField dateF) {
17 ilm 376
                final Calendar date = dateF == null ? null : row.getDate(dateF.getName());
156 ilm 377
                final SQLRowAccessor user = userF == null || row.getObject(userF.getName()) == null || row.isForeignEmpty(userF.getName()) ? null : row.getForeign(userF.getName());
17 ilm 378
                if (user == null && date == null)
379
                    return null;
380
 
73 ilm 381
                final int userParam;
382
                final String firstName, lastName;
383
                if (user != null) {
384
                    userParam = 1;
385
                    firstName = user.getString("PRENOM");
386
                    lastName = user.getString("NOM");
387
                } else {
388
                    userParam = 0;
389
                    firstName = null;
390
                    lastName = null;
391
                }
17 ilm 392
 
73 ilm 393
                return TM.tr("ilist.metadata", created ? 1 : 0, userParam, firstName, lastName, date == null ? 0 : 1, date == null ? null : date.getTime());
17 ilm 394
            }
395
 
396
            @Override
93 ilm 397
            protected TableColumnModel createDefaultColumnModel() {
398
                // allow to hide columns
399
                return new XTableColumnModel();
400
            }
401
 
402
            // only load from XML once
403
            private boolean stateLoaded = false;
404
 
405
            @Override
17 ilm 406
            public void createDefaultColumnsFromModel() {
93 ilm 407
                final XTableColumnModel cm = getColumnModel() instanceof XTableColumnModel ? (XTableColumnModel) getColumnModel() : null;
408
                final Set<Object> invisibleCols = new HashSet<Object>();
409
                if (cm != null) {
410
                    // Remove any current columns, including invisible ones
411
                    while (cm.getColumnCount(false) > 0) {
412
                        final TableColumn col = cm.getColumn(0, false);
413
                        if (!cm.isColumnVisible(col)) {
414
                            if (!invisibleCols.add(col.getIdentifier()))
415
                                throw new IllegalStateException("Duplicate identifier " + col.getIdentifier());
416
                        }
417
                        cm.removeColumn(col);
418
                    }
419
                }
17 ilm 420
                super.createDefaultColumnsFromModel();
93 ilm 421
                final boolean stateLoadedByThisMethod;
422
                // don't try to load state from XML, when e.g. the list is switching to debug
423
                if (this.stateLoaded) {
424
                    stateLoadedByThisMethod = false;
425
                } else {
426
                    // only load when all columns are created
427
                    stateLoadedByThisMethod = loadTableState();
428
                    this.stateLoaded = stateLoadedByThisMethod;
429
                }
430
                // don't overwrite state loaded by XML
431
                if (!stateLoadedByThisMethod && cm != null) {
432
                    for (final TableColumn col : new ArrayList<TableColumn>(cm.getColumns(false))) {
433
                        cm.setColumnVisible(col, !invisibleCols.contains(col.getIdentifier()));
434
                    }
435
                }
17 ilm 436
            };
437
        };
438
        this.adjustVisible = true;
439
        this.tcsa = null;
440
        this.filter = new JTextField();
441
        this.filter.setEditable(false);
442
        this.debugFilter = false;
443
 
444
        // do not handle F2, let our application use it :
445
        // remove F2 keybinding, use space
446
        final InputMap tm = this.jTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
447
        remove(tm, KeyStroke.getKeyStroke("F2"));
448
        tm.put(KeyStroke.getKeyStroke(' '), "startEditing");
449
        // don't auto start, otherwise F2 will trigger the edition
450
        this.jTable.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
451
 
25 ilm 452
        // Better look
453
        this.jTable.setShowHorizontalLines(false);
454
        this.jTable.setGridColor(new Color(230, 230, 230));
151 ilm 455
        this.jTable.setRowHeight(FontUtils.getPreferredRowHeight(this.jTable));
25 ilm 456
 
17 ilm 457
        this.popup = new JPopupMenu();
458
        TablePopupMouseListener.add(this.jTable, new ITransformer<MouseEvent, JPopupMenu>() {
459
            @Override
460
            public JPopupMenu transformChecked(MouseEvent input) {
21 ilm 461
                return updatePopupMenu(true);
17 ilm 462
            }
463
        });
19 ilm 464
        this.jTable.addMouseListener(new MouseAdapter() {
465
            @Override
466
            public void mouseClicked(MouseEvent e) {
467
                if (e.getClickCount() == 2)
468
                    performDefaultAction(e);
469
            }
470
        });
17 ilm 471
 
472
        this.selectionListener = new ListSelectionListener() {
473
            public void valueChanged(ListSelectionEvent e) {
474
                if (!e.getValueIsAdjusting()) {
475
                    fireNASelectionId();
19 ilm 476
                    updateButtons();
17 ilm 477
                }
478
            }
479
        };
80 ilm 480
        this.selectionDataListener = new TableModelListener() {
481
            @Override
482
            public void tableChanged(TableModelEvent e) {
483
                // assert we're not listening to the sorter since we're interested in data change
484
                // not sort order
485
                assert e.getSource() instanceof ITableModel;
486
                boolean fire = false;
487
                // insert or delete don't change the content of current selection (e.g. if the
488
                // deleted row was part of the selection "selectedIDs" will change)
489
                // a change in the name or order of columns doesn't mean the SQL values are updated
490
                if (e.getType() == TableModelEvent.UPDATE && e.getFirstRow() != TableModelEvent.HEADER_ROW) {
491
                    // see TableModelEvent(TableModel) constructor
492
                    if (e.getLastRow() == Integer.MAX_VALUE) {
493
                        // since JTable uses a regular listener to update its selection and the
494
                        // listeners are called in reverse order, the selection isn't yet cleared by
495
                        // JTable.tableChanged(). Thus if the table was just shrunk, the selection
496
                        // might be out of bounds. So don't fire now, let
497
                        // JTable.clearSelectionAndLeadAnchor() do it.
498
                        fire = false;
499
                    } else {
500
                        // do fire if only some rows were updated as in this case, no selection
501
                        // change will occur.
502
                        for (int i = e.getFirstRow(); !fire && i <= e.getLastRow(); i++) {
503
                            if (getJTable().getSelectionModel().isSelectedIndex(IListe.this.sorter.viewIndex(i)))
504
                                fire = true;
505
                        }
506
                    }
507
                }
508
                if (fire)
509
                    IListe.this.supp.firePropertyChange(SELECTION_DATA_PROPNAME, null, null);
510
            }
511
        };
17 ilm 512
        this.filterListener = new PropertyChangeListener() {
513
            public void propertyChange(PropertyChangeEvent evt) {
514
                updateFilter();
515
            }
516
        };
517
        this.jTable.getColumnModel().addColumnModelListener(new TableColumnModelAdapter() {
518
            // invoked by toggleAutoAdjust(), ITableModel.setDebug() or updateColNames()
519
            @Override
520
            public void columnAdded(TableColumnModelEvent e) {
521
                updateCols(e.getToIndex());
522
            }
523
        });
65 ilm 524
        this.tableStateManager = new JTableStateManager(this.jTable);
525
        this.setConfigFile(configFile);
17 ilm 526
 
527
        // MAYBE only set this.src and let the model be null so that the mere creation of an IListe
528
        // does not spawn several threads and access the db. But a lot of code assumes there's
529
        // immediately a model.
142 ilm 530
        this.setSource(req);
17 ilm 531
        this.state = ListSelectionState.manage(this.jTable.getSelectionModel(), new TableListStateModel(this.sorter));
532
        this.state.addPropertyChangeListener("selectedIndex", new PropertyChangeListener() {
533
            public void propertyChange(PropertyChangeEvent evt) {
534
                final Number newValue = (Number) evt.getNewValue();
535
                // if there's no selection (eg some lines were removed)
536
                // don't try to scroll (it will go to the top)
537
                if (newValue.intValue() >= 0)
538
                    IListe.this.jTable.scrollRectToVisible(IListe.this.jTable.getCellRect(newValue.intValue(), 0, true));
539
            }
540
        });
541
        this.state.addPropertyChangeListener("selectedID", new PropertyChangeListener() {
542
            public void propertyChange(PropertyChangeEvent evt) {
543
                fireSelectionId(((Number) evt.getNewValue()).intValue(), IListe.this.jTable.getSelectedColumn());
544
            }
545
        });
80 ilm 546
        // don't use userSelectedIDs as we need to fire when the whole list is changed, see
547
        // this.selectionDataListener
548
        this.state.addPropertyChangeListener("selectedIDs", new PropertyChangeListener() {
549
            public void propertyChange(PropertyChangeEvent evt) {
550
                IListe.this.supp.firePropertyChange(SELECTION_DATA_PROPNAME, null, null);
551
            }
552
        });
17 ilm 553
        // this.jTable.setEnabled(!updating) ne sert à rien
554
        // car les updates du ITableModel se font de manière synchrone dans la EDT
555
        // donc on ne peut faire aucune action pendant les maj
556
 
19 ilm 557
        this.btnPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
151 ilm 558
        this.addListenerOnModel(new PropertyChangeListener() {
21 ilm 559
            @Override
560
            public void propertyChange(PropertyChangeEvent evt) {
561
                // let the header buttons know that the rows have changed
151 ilm 562
                final boolean doneUpdating = "updating".equals(evt.getPropertyName()) && Boolean.FALSE.equals(evt.getNewValue());
563
                if (doneUpdating || "cellsEditable".equals(evt.getPropertyName()))
21 ilm 564
                    updateButtons();
565
            }
566
        });
28 ilm 567
        this.searchFormats = new HashMap<Class<?>, FormatGroup>(this.getFormats());
568
        // localized boolean search
569
        this.searchFormats.put(Boolean.class, new FormatGroup(new BooleanFormat(), BooleanFormat.getNumberInstance(), BooleanFormat.createYesNo(Locale.getDefault())));
570
        // on edition we want to force the user to enter a time, so it doesn't blindly paste a date
571
        // and erase the time part. But on search it's quicker to filter with > 25/12/99
572
        final List<Format> wAndwoTime = new ArrayList<Format>();
573
        wAndwoTime.addAll(this.searchFormats.get(Timestamp.class).getFormats());
574
        wAndwoTime.addAll(this.searchFormats.get(Date.class).getFormats());
575
        this.searchFormats.put(Timestamp.class, new FormatGroup(wAndwoTime));
19 ilm 576
 
17 ilm 577
        uiInit();
578
    }
579
 
580
    /**
581
     * Formats used for editing cells.
582
     *
583
     * @return a mapping between cell value's class and its format.
584
     */
585
    public final Map<Class<?>, FormatGroup> getFormats() {
586
        return FORMATS;
587
    }
588
 
28 ilm 589
    public final Map<Class<?>, FormatGroup> getSearchFormats() {
590
        return this.searchFormats;
591
    }
592
 
19 ilm 593
    public final RowAction addRowAction(Action action) {
67 ilm 594
        return this.addRowAction(action, null);
595
    }
596
 
597
    public final RowAction addRowAction(Action action, String id) {
19 ilm 598
        // for backward compatibility don't put in header
67 ilm 599
        final RowAction res = new PredicateRowAction(action, false, true, id).setPredicate(IListeEvent.getSingleSelectionPredicate());
21 ilm 600
        this.addIListeAction(res);
19 ilm 601
        return res;
17 ilm 602
    }
603
 
21 ilm 604
    public final void addIListeActions(Collection<? extends IListeAction> actions) {
605
        for (final IListeAction a : actions)
606
            this.addIListeAction(a);
19 ilm 607
    }
608
 
21 ilm 609
    private final int findGroupIndex(final String groupName) {
610
        if (groupName != null) {
611
            final Component[] components = this.btnPanel.getComponents();
612
            for (int i = components.length - 1; i >= 0; i--) {
613
                final JComponent comp = (JComponent) components[i];
614
                if (groupName.equals(comp.getClientProperty(ButtonsBuilder.GROUPNAME_PROPNAME))) {
615
                    return i + 1;
616
                }
617
            }
618
        }
619
        return -1;
19 ilm 620
    }
621
 
21 ilm 622
    public final void addIListeAction(IListeAction action) {
623
        // we need to handle addition of an already added action at least for setDefaultRowAction()
624
        if (this.rowActions.containsKey(action))
625
            return;
626
        final ButtonsBuilder headerBtns = action.getHeaderButtons();
627
        this.rowActions.put(action, headerBtns);
628
        if (headerBtns.getContent().size() > 0) {
629
            updateButton(headerBtns, new IListeEvent(this));
630
            for (final JButton headerBtn : headerBtns.getContent().keySet()) {
28 ilm 631
                headerBtn.setOpaque(false);
21 ilm 632
                this.btnPanel.add(headerBtn, findGroupIndex((String) headerBtn.getClientProperty(ButtonsBuilder.GROUPNAME_PROPNAME)));
633
            }
19 ilm 634
            this.btnPanel.setVisible(true);
635
        }
636
    }
637
 
21 ilm 638
    public final void removeIListeActions(Collection<? extends IListeAction> actions) {
639
        for (final IListeAction a : actions)
640
            this.removeIListeAction(a);
19 ilm 641
    }
642
 
21 ilm 643
    public final void removeIListeAction(IListeAction action) {
644
        final ButtonsBuilder headerBtns = this.rowActions.remove(action);
645
        // handle the removal of inexistent action (ButtonsBuilder can not be null)
646
        if (headerBtns == null)
647
            return;
648
        for (final JButton headerBtn : headerBtns.getContent().keySet()) {
19 ilm 649
            this.btnPanel.remove(headerBtn);
650
            if (this.btnPanel.getComponentCount() == 0)
651
                this.btnPanel.setVisible(false);
652
            this.btnPanel.revalidate();
653
        }
654
        if (action.equals(this.defaultRowAction))
21 ilm 655
            this.setDefaultRowAction(null);
19 ilm 656
    }
657
 
658
    private void updateButtons() {
21 ilm 659
        final IListeEvent evt = new IListeEvent(this);
660
        for (final ButtonsBuilder btns : this.rowActions.values()) {
661
            this.updateButton(btns, evt);
19 ilm 662
        }
663
    }
664
 
21 ilm 665
    private void updateButton(final ButtonsBuilder btns, final IListeEvent evt) {
666
        for (final Entry<JButton, IPredicate<IListeEvent>> e : btns.getContent().entrySet()) {
667
            e.getKey().setEnabled(e.getValue().evaluateChecked(evt));
19 ilm 668
        }
669
    }
670
 
21 ilm 671
    private JPopupMenu updatePopupMenu(final boolean onRows) {
19 ilm 672
        this.popup.removeAll();
21 ilm 673
        final PopupEvent evt = new PopupEvent(this, onRows);
674
        final Action defaultAction = this.defaultRowAction != null ? this.defaultRowAction.getDefaultAction(evt) : null;
675
        final VirtualMenu menu = VirtualMenu.createRoot(null);
676
        for (final IListeAction a : this.rowActions.keySet()) {
677
            final PopupBuilder popupContent = a.getPopupContent(evt);
678
            if (defaultAction != null && a == this.defaultRowAction) {
151 ilm 679
                /**
680
                 * If popup actions are ["Dial 03", "Dial 06"] then getDefaultAction() must not
681
                 * always return the same instance "if land line is default then Dial 03 else Dial
682
                 * 06" otherwise we can't find its matching menu item in the popup. IOW the check
683
                 * should be done in getDefaultAction(), which should then return one of the popup
684
                 * actions.
685
                 * <p>
686
                 * Also, the IListeAction can just choose not to return the default action in its
687
                 * menu.
688
                 */
21 ilm 689
                final JMenuItem defaultMI = popupContent.getRootMenuItem(defaultAction);
690
                if (defaultMI == null)
151 ilm 691
                    Log.get().info("Default action not found at the root level of popup for " + this);
21 ilm 692
                else
693
                    defaultMI.setFont(defaultMI.getFont().deriveFont(Font.BOLD));
19 ilm 694
            }
21 ilm 695
            menu.merge(popupContent.getMenu());
19 ilm 696
        }
697
 
21 ilm 698
        for (final Entry<JMenuItem, List<String>> e : menu.getContent().entrySet()) {
699
            MenuUtils.addMenuItem(e.getKey(), this.popup, e.getValue());
19 ilm 700
        }
701
 
702
        return this.popup;
703
    }
704
 
705
    /**
21 ilm 706
     * Set the action performed when double-clicking a row.
19 ilm 707
     *
21 ilm 708
     * @param action the default action, can be <code>null</code>.
19 ilm 709
     */
21 ilm 710
    public final void setDefaultRowAction(final IListeAction action) {
19 ilm 711
        this.defaultRowAction = action;
21 ilm 712
        if (action != null)
713
            this.addIListeAction(action);
19 ilm 714
    }
715
 
21 ilm 716
    public final IListeAction getDefaultRowAction() {
19 ilm 717
        return this.defaultRowAction;
718
    }
719
 
720
    private void performDefaultAction(MouseEvent e) {
21 ilm 721
        // special method needed since sometimes getPopupContent() can access the DB (optionally
722
        // creating threads) or be slow
723
        if (this.defaultRowAction != null) {
724
            final Action defaultAction = this.defaultRowAction.getDefaultAction(new IListeEvent(this));
725
            if (defaultAction != null)
726
                defaultAction.actionPerformed(new ActionEvent(e.getSource(), e.getID(), null, e.getWhen(), e.getModifiers()));
727
        }
19 ilm 728
    }
729
 
17 ilm 730
    private void uiInit() {
731
        // * filter
732
        this.filter.addMouseListener(new MouseAdapter() {
733
            @Override
734
            public void mouseClicked(MouseEvent e) {
735
                if (e.isAltDown()) {
736
                    invertDebug();
737
                }
738
            }
739
        });
740
        FontUtils.setFontFor(this.filter, SEP);
142 ilm 741
        // initially hide to limit modifications for instances which don't need the filter, see
742
        // setFilter() comment
743
        this.setFilter(null);
17 ilm 744
        this.updateFilter();
745
 
746
        // * JTable
747
 
748
        // active/désactive le mode DEBUG du tableModel en ALT-clickant sur les entêtes des colonnes
749
        this.jTable.getTableHeader().addMouseListener(new MouseAdapter() {
93 ilm 750
 
751
            @Override
17 ilm 752
            public void mouseClicked(MouseEvent e) {
753
                if (e.isAltDown()) {
754
                    final boolean debug = IListe.this.getModel().isDebug();
755
                    IListe.this.getModel().setDebug(!debug);
756
                    setDebug(!debug);
757
                }
758
            }
759
 
93 ilm 760
            static private final String COL_INDEX_KEY = "tableColIndex";
761
 
17 ilm 762
            private final JPopupMenu popupMenu;
93 ilm 763
            private final Action toggleWidth;
764
            private final Action toggleVisibility;
765
            private final Action setAllVisible;
766
 
17 ilm 767
            {
768
                this.popupMenu = new JPopupMenu();
93 ilm 769
                this.toggleWidth = new AbstractAction(TM.tr("ilist.setColumnsWidth")) {
17 ilm 770
                    @Override
771
                    public void actionPerformed(ActionEvent e) {
772
                        toggleAutoAdjust();
773
                    }
93 ilm 774
                };
775
                this.toggleVisibility = new AbstractAction() {
776
                    @Override
777
                    public void actionPerformed(ActionEvent e) {
778
                        final XTableColumnModel colModel = (XTableColumnModel) getJTable().getColumnModel();
779
                        final JComponent cb = (JComponent) e.getSource();
780
                        final int columnIndex = ((Number) cb.getClientProperty(COL_INDEX_KEY)).intValue();
781
                        final TableColumn col = colModel.getColumn(columnIndex, false);
782
                        final boolean newValue = !colModel.isColumnVisible(col);
783
                        // don't remove last column
784
                        if (newValue || colModel.getColumnCount(true) > 1)
785
                            colModel.setColumnVisible(col, newValue);
786
                    }
787
                };
788
                this.setAllVisible = new AbstractAction() {
789
                    @Override
790
                    public void actionPerformed(ActionEvent e) {
791
                        final XTableColumnModel colModel = (XTableColumnModel) getJTable().getColumnModel();
792
                        colModel.setAllColumnsVisible();
793
                    }
794
                };
17 ilm 795
            }
796
 
797
            @Override
798
            public void mousePressed(MouseEvent e) {
799
                maybeShowPopup(e);
800
            }
801
 
802
            @Override
803
            public void mouseReleased(MouseEvent e) {
804
                maybeShowPopup(e);
805
            }
806
 
807
            private void maybeShowPopup(MouseEvent e) {
93 ilm 808
                if (e.isPopupTrigger()) {
809
                    this.popupMenu.removeAll();
810
 
811
                    if (IListe.this.adjustVisible) {
812
                        final JCheckBoxMenuItem cb = new JCheckBoxMenuItem(this.toggleWidth);
813
                        cb.setSelected(isAutoAdjusting());
814
                        this.popupMenu.add(cb);
815
                    }
816
 
817
                    if (getJTable().getColumnModel() instanceof XTableColumnModel) {
818
                        if (this.popupMenu.getComponentCount() > 0)
819
                            this.popupMenu.addSeparator();
820
                        this.setAllVisible.putValue(Action.NAME, TM.tr("ilist.showAllColumns"));
821
                        this.popupMenu.add(this.setAllVisible);
822
 
823
                        final XTableColumnModel colModel = (XTableColumnModel) getJTable().getColumnModel();
824
                        int i = 0;
825
                        final boolean disableLastCol = colModel.getColumnCount(true) == 1;
826
                        for (final TableColumn c : colModel.getColumns(false)) {
827
                            final JCheckBoxMenuItem cb = new JCheckBoxMenuItem(this.toggleVisibility);
828
                            // speed up display of menu
829
                            cb.setText(StringUtils.Shortener.Ellipsis.getBoundedLengthString(String.valueOf(c.getHeaderValue()), 200));
830
                            final boolean isVisible = colModel.isColumnVisible(c);
831
                            cb.setSelected(isVisible);
832
                            cb.setEnabled(!isVisible || !disableLastCol);
833
                            if (!cb.isEnabled())
834
                                cb.setToolTipText(TM.tr("ilist.lastCol"));
835
                            cb.putClientProperty(COL_INDEX_KEY, i++);
836
                            this.popupMenu.add(cb);
837
                        }
838
                    }
839
 
840
                    if (this.popupMenu.getComponentCount() > 0)
841
                        this.popupMenu.show((Component) e.getSource(), e.getX(), e.getY());
17 ilm 842
                }
843
            }
844
 
845
        });
846
        // use SQLTableModelColumn.getToolTip()
847
        this.jTable.getTableHeader().setDefaultRenderer(new TableCellRenderer() {
848
            private final TableCellRenderer orig = IListe.this.jTable.getTableHeader().getDefaultRenderer();
849
 
850
            @Override
851
            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
852
                final Component res = this.orig.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
853
                if (res instanceof JComponent) {
854
                    // column is the view index
855
                    final SQLTableModelColumn col = getSource().getColumn(table.convertColumnIndexToModel(column));
856
                    ((JComponent) res).setToolTipText(col.getToolTip());
857
                }
858
                return res;
859
            }
860
        });
861
        this.jTable.setDefaultRenderer(Clob.class, new DefaultTableCellRenderer() {
862
            @Override
863
            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
864
                return super.getTableCellRendererComponent(table, StringClobConvertor.INSTANCE.unconvert((Clob) value), isSelected, hasFocus, row, column);
865
            }
866
        });
80 ilm 867
        this.jTable.setDefaultRenderer(Date.class, createDateRenderer());
868
        this.jTable.setDefaultRenderer(Time.class, createTimeRenderer());
869
        this.jTable.setDefaultRenderer(Timestamp.class, createDateTimeRenderer());
17 ilm 870
        for (final Map.Entry<Class<?>, FormatGroup> e : this.getFormats().entrySet())
871
            this.jTable.setDefaultEditor(e.getKey(), new FormatEditor(e.getValue()));
872
        this.sorter.setTableHeader(this.jTable.getTableHeader());
63 ilm 873
        this.addAncestorListener(new AncestorListener() {
874
 
875
            // these callbacks are called later than the change, and by that time the visibility
65 ilm 876
            // might have changed several times thus use isShowing() to avoid flip-flopping for
63 ilm 877
            // nothing
878
 
879
            @Override
880
            public void ancestorRemoved(AncestorEvent event) {
881
                visibilityChanged();
882
            }
883
 
884
            @Override
885
            public void ancestorAdded(AncestorEvent event) {
886
                visibilityChanged();
887
            }
888
 
889
            @Override
890
            public void ancestorMoved(AncestorEvent event) {
891
                // nothing to do
892
            }
893
        });
17 ilm 894
        // we used to rm this listener, possibly to avoid events once dead, but this doesn't seem
895
        // necessary anymore
896
        this.jTable.getSelectionModel().addListSelectionListener(this.selectionListener);
897
 
898
        // TODO speed up like IListPanel buttons
899
        // works because "JTable.autoStartsEdit" is false
900
        // otherwise mets un + a la fin de la cellule courante
151 ilm 901
        if (this.getSource().getReq().isTableOrder()) {
93 ilm 902
            this.jTable.addKeyListener(new KeyAdapter() {
903
                public void keyTyped(KeyEvent e) {
904
                    if (e.getKeyChar() == '+') {
905
                        deplacerDe(1);
906
                    } else if (e.getKeyChar() == '-') {
907
                        deplacerDe(-1);
908
                    }
17 ilm 909
                }
93 ilm 910
            });
17 ilm 911
 
93 ilm 912
            // DnD
913
            this.jTable.setDragEnabled(true);
914
            this.jTable.setDropMode(DropMode.INSERT_ROWS);
915
            this.jTable.setTransferHandler(new IListeTransferHandler());
916
        }
917
 
17 ilm 918
        final JScrollPane scrollPane = new JScrollPane(this.jTable);
919
        scrollPane.setFocusable(false);
21 ilm 920
        scrollPane.addMouseListener(new PopupMouseListener() {
921
            @Override
922
            protected JPopupMenu createPopup(MouseEvent e) {
923
                return updatePopupMenu(false);
924
            }
925
        });
17 ilm 926
 
927
        this.setLayout(new GridBagLayout());
928
        final GridBagConstraints c = new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0);
929
        this.add(this.filter, c);
19 ilm 930
 
931
        c.gridy++;
932
        this.btnPanel.setVisible(false);
28 ilm 933
        this.btnPanel.setOpaque(false);
19 ilm 934
        this.add(this.btnPanel, c);
935
 
17 ilm 936
        c.weighty = 1;
937
        c.gridy++;
938
        this.add(scrollPane, c);
939
 
940
        // destroy if non displayable
941
        this.addHierarchyListener(new HierarchyListener() {
942
            public void hierarchyChanged(HierarchyEvent e) {
943
                if ((e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0)
944
                    dispChanged();
945
            }
946
        });
19 ilm 947
 
65 ilm 948
        this.setOpaque(false);
19 ilm 949
        this.setTransferHandler(new FileTransfertHandler(getSource().getPrimaryTable()));
80 ilm 950
 
951
        if (this.getSource().getPrimaryTable().getFieldRaw(SQLComponent.READ_ONLY_FIELD) != null) {
952
            this.addIListeAction(getUnlockAction());
953
            this.addIListeAction(getLockAction());
954
        }
17 ilm 955
    }
956
 
957
    protected synchronized final void invertDebug() {
958
        this.setDebug(!this.debugFilter);
959
    }
960
 
961
    protected synchronized final void setDebug(boolean b) {
962
        this.debugFilter = b;
963
        updateFilter();
964
    }
965
 
966
    // thread-safe
967
    private synchronized void updateFilter() {
968
        if (this.filterWorker != null) {
969
            this.filterWorker.cancel(true);
970
        }
971
        final FilterWorker worker;
972
        if (!this.hasRequest()) {
973
            worker = new RowFilterWorker(null);
974
        } else if (this.debugFilter) {
975
            worker = new WhereFilterWorker(this.getRequest().getInstanceWhere());
976
        } else {
977
            worker = new RowFilterWorker(this.getRequest().getFilterRows());
978
        }
979
        this.filterWorker = worker;
980
        this.filterWorker.execute();
981
    }
982
 
983
    /**
984
     * Sets the filter label.
985
     *
986
     * @param text the text to display, <code>null</code> to hide the label.
987
     */
988
    private void setFilter(String text) {
142 ilm 989
        final boolean currentVisible = this.filter.isVisible();
990
        final boolean newVisible = text != null;
991
        // limit modifications due to a bug in Swing that can bring the frame to the back.
992
        if (newVisible || currentVisible) {
993
            this.filter.setText(text == null ? "" : text);
994
            this.filter.setVisible(newVisible);
995
            this.revalidate();
996
        }
17 ilm 997
    }
998
 
999
    public void selectID(final int id) {
1000
        this.selectIDs(Collections.singleton(id));
1001
    }
1002
 
1003
    public void selectIDs(final Collection<Integer> ids) {
1004
        if (!SwingUtilities.isEventDispatchThread())
1005
            throw new IllegalStateException("not in EDT");
1006
        // no need to put a runnable in the model queue to wait for an inserted ID to actually
1007
        // show up in the list, the ListSelectionState will record the userID and select it after
1008
        // the update
1009
        if (!isDead())
1010
            this.state.selectIDs(ids);
1011
    }
1012
 
1013
    // retourne l'ID de la ligne rowIndex à l'écran.
1014
    public int idFromIndex(int rowIndex) {
1015
        return this.state.idFromIndex(rowIndex);
1016
    }
1017
 
1018
    /**
1019
     * Cherche une chaîne de caractères dans la liste et reclasse les éléments trouvés au début
1020
     *
1021
     * @param s la chaîne de caractères recherchées
1022
     * @param column la colonne dans laquelle chercher, <code>null</code> pour toutes.
1023
     */
1024
    public void search(String s, String column) {
1025
        this.search(s, column, null);
1026
    }
1027
 
1028
    public void search(String s, String column, Runnable r) {
1029
        // Determine sur quelle colonne on cherche
93 ilm 1030
        this.getModel().searchContains(s, this.getModel().getColumnNames().indexOf(column), r);
17 ilm 1031
    }
1032
 
1033
    // Export en tableau OpenOffice
1034
    public void exporter(File file) throws IOException {
19 ilm 1035
        exporter(file, false, XMLFormatVersion.getDefault());
17 ilm 1036
    }
1037
 
19 ilm 1038
    public File exporter(File file, final boolean onlySelection, final XMLFormatVersion version) throws IOException {
17 ilm 1039
        return SpreadSheet.export(getExportModel(onlySelection), file, version);
1040
    }
1041
 
1042
    protected TableModel getExportModel(final boolean onlySelection) {
19 ilm 1043
        final ViewTableModel res = new ViewTableModel(this.jTable);
17 ilm 1044
        return onlySelection ? new TableModelSelectionAdapter(res, this.jTable.getSelectedRows()) : res;
1045
    }
1046
 
1047
    public void update() {
1048
        this.getModel().updateAll();
1049
    }
1050
 
1051
    /**
1052
     * Retourne le nombre de ligne de cette liste.
1053
     *
93 ilm 1054
     * @return le nombre de ligne de cette liste, -1 si {@link #isDead()}.
17 ilm 1055
     */
1056
    public int getRowCount() {
93 ilm 1057
        if (isDead()) {
1058
            return -1;
1059
        }
17 ilm 1060
        return this.getTableModel().getRowCount();
1061
    }
1062
 
1063
    public int getTotalRowCount() {
1064
        if (isDead()) {
93 ilm 1065
            return -1;
17 ilm 1066
        }
1067
        return this.getModel().getTotalRowCount();
1068
    }
1069
 
1070
    public final boolean isDead() {
1071
        return this.getTableModel() == null;
1072
    }
1073
 
1074
    /**
1075
     * Retourne le nombre d'éléments contenu dans cette liste. C'est à dire la somme du champs
1076
     * 'quantité' ou 'nombre d'essai DDR'.
1077
     *
1078
     * @return la somme ou -1 s'il n'y a pas de champs quantité.
1079
     */
1080
    public int getItemCount() {
1081
        int count = -1;
1082
        if (!this.isDead()) {
1083
            int fieldIndex = -1;
1084
            // ATTN ne marche que si qte est dans les listFields, donc dans le tableModel
1085
            // sinon on pourrait faire un SUM(QUANTITE)
1086
            final SQLField qte;
1087
            final SQLTable t = this.getModel().getTable();
1088
            if (t.contains("QUANTITE"))
1089
                qte = t.getField("QUANTITE");
1090
            else
1091
                qte = t.getFieldRaw("NB_ESSAI_DDR");
1092
 
1093
            if (qte != null) {
1094
                int i = 0;
93 ilm 1095
                for (final SQLTableModelColumn col : this.getModel().getCols()) {
17 ilm 1096
                    if (CollectionUtils.getSole(col.getFields()) == qte)
1097
                        fieldIndex = i;
1098
                    i++;
1099
                }
1100
            }
1101
            if (fieldIndex > 0) {
1102
                count = 0;
1103
                for (int j = 0; j < this.getTableModel().getRowCount(); j++) {
1104
                    count += ((Number) this.getTableModel().getValueAt(j, fieldIndex)).intValue();
1105
                }
1106
            }
1107
        }
1108
        return count;
1109
    }
1110
 
1111
    public void deplacerDe(final int inc) {
93 ilm 1112
        if (isSorted())
1113
            return;
1114
        this.getModel().moveBy(this.getSelectedRows(), inc, true);
17 ilm 1115
    }
1116
 
1117
    /**
1118
     * The currently selected id.
1119
     *
1120
     * @return the currently selected id or -1 if no selection.
1121
     */
1122
    public int getSelectedId() {
1123
        return this.state.getSelectedID();
1124
    }
1125
 
1126
    public final boolean hasSelection() {
1127
        return this.jTable.getSelectedRow() >= 0;
1128
    }
1129
 
1130
    public final ListSelection getSelection() {
1131
        return this.state;
1132
    }
1133
 
1134
    /**
1135
     * Return the line at the passed index.
1136
     *
1137
     * @param viewIndex the index in the JTable.
1138
     * @return the line at the passed index.
1139
     * @see ITableModel#getLine(TableModel, int)
1140
     */
1141
    public final ListSQLLine getLine(int viewIndex) {
1142
        return ITableModel.getLine(this.getJTable().getModel(), viewIndex);
1143
    }
1144
 
80 ilm 1145
    // protect our internal values
1146
    private <R> R getRow(int index, final Class<R> clazz) {
151 ilm 1147
        final ListSQLLine line = this.getLine(index);
1148
        final Object toCast;
80 ilm 1149
        if (clazz == SQLRowValues.class) {
151 ilm 1150
            toCast = line.getRow().toImmutable();
80 ilm 1151
        } else if (clazz == SQLRow.class) {
151 ilm 1152
            toCast = line.getRow().asRow();
1153
        } else if (clazz == ListSQLLine.class) {
1154
            toCast = line;
80 ilm 1155
        } else {
93 ilm 1156
            throw new IllegalArgumentException("Not implemented : " + clazz);
80 ilm 1157
        }
1158
        return clazz.cast(toCast);
1159
    }
1160
 
142 ilm 1161
    private SQLRow fetchRow(int id) {
17 ilm 1162
        if (id < SQLRow.MIN_VALID_ID) {
1163
            return null;
1164
        } else
1165
            return this.getSource().getPrimaryTable().getRow(id);
1166
    }
1167
 
80 ilm 1168
    public SQLRow fetchSelectedRow() {
1169
        return this.fetchRow(this.getSelectedId());
17 ilm 1170
    }
1171
 
93 ilm 1172
    public SQLRowValues getSelectedRow() {
80 ilm 1173
        return this.getSelectedRow(SQLRowValues.class);
1174
    }
1175
 
1176
    // selected row cannot be inferred from iterateSelectedRows() since the user might have selected
1177
    // the last row anywhere in the selection
1178
    private final <R extends SQLRowAccessor> R getSelectedRow(final Class<R> clazz) {
1179
        final int selectedIndex = this.state.getSelectedIndex().intValue();
1180
        if (selectedIndex == BaseListStateModel.INVALID_INDEX)
1181
            return null;
1182
        else
1183
            return this.getRow(selectedIndex, clazz);
1184
    }
1185
 
17 ilm 1186
    public final SQLRow getDesiredRow() {
80 ilm 1187
        return this.fetchRow(this.getSelection().getUserSelectedID());
17 ilm 1188
    }
1189
 
93 ilm 1190
    public final List<SQLRowValues> getSelectedRows() {
73 ilm 1191
        return iterateSelectedRows(SQLRowValues.class);
1192
    }
1193
 
151 ilm 1194
    public final List<ListSQLLine> getSelectedLines() {
1195
        return iterateSelectedRows(ListSQLLine.class);
1196
    }
1197
 
1198
    private final <R> List<R> iterateSelectedRows(final Class<R> clazz) {
19 ilm 1199
        final ListSelectionModel selectionModel = this.getJTable().getSelectionModel();
1200
        if (selectionModel.isSelectionEmpty())
1201
            return Collections.emptyList();
1202
 
1203
        final int start = selectionModel.getMinSelectionIndex();
1204
        final int stop = selectionModel.getMaxSelectionIndex();
73 ilm 1205
        final List<R> res = new ArrayList<R>();
19 ilm 1206
        for (int i = start; i <= stop; i++) {
73 ilm 1207
            if (selectionModel.isSelectedIndex(i)) {
94 ilm 1208
                try {
1209
                    res.add(getRow(i, clazz));
132 ilm 1210
                } catch (IndexOutOfBoundsException e) {
174 ilm 1211
                    throw new IllegalStateException("The selected row at " + i
1212
                            + " is not in the model : it has been changed before Swing could update the selection. E.g. the DB was changed on mousePressed and Swing updated the selection on mouseReleased.",
132 ilm 1213
                            e);
94 ilm 1214
                }
73 ilm 1215
            }
19 ilm 1216
        }
1217
        return res;
1218
    }
1219
 
17 ilm 1220
    public final void setAdjustVisible(boolean b) {
1221
        this.adjustVisible = b;
1222
    }
1223
 
1224
    protected final void toggleAutoAdjust() {
1225
        if (this.tcsa == null) {
1226
            this.tcsa = new ColumnSizeAdjustor(this.jTable);
1227
        } else {
1228
            this.tcsa.setInstalled(!this.tcsa.isInstalled());
1229
        }
1230
    }
1231
 
1232
    public final boolean isAutoAdjusting() {
1233
        if (this.tcsa == null) {
1234
            return false;
1235
        } else
1236
            return this.tcsa.isInstalled();
1237
    }
1238
 
1239
    // *** Listeners ***//
1240
 
1241
    public void addIListener(IListener l) {
1242
        this.listeners.add(l);
1243
    }
1244
 
1245
    public void addNonAdjustingIListener(IListener l) {
1246
        this.naListeners.add(l);
1247
    }
1248
 
80 ilm 1249
    /**
1250
     * Adds a listener to the list that's notified each time a change to the data model occurs. This
1251
     * includes when this is not displayable and the model becomes empty.
1252
     *
1253
     * @param l the listener.
1254
     * @see #retain()
1255
     */
17 ilm 1256
    public void addListener(TableModelListener l) {
80 ilm 1257
        // sorter is final, only its own model (ITableModel) changes
1258
        this.sorter.addTableModelListener(l);
17 ilm 1259
    }
1260
 
80 ilm 1261
    public void removeListener(TableModelListener l) {
1262
        this.sorter.removeTableModelListener(l);
1263
    }
1264
 
17 ilm 1265
    /**
1266
     * To be notified when the table is being sorted. Each time a sort is requested you'll be
1267
     * notified twice to indicate the beginning and end of the sort. Don't confuse it with the
1268
     * sortED status.
1269
     *
1270
     * @param l the listener.
1271
     * @see #isSorted()
1272
     */
1273
    public void addSortListener(PropertyChangeListener l) {
1274
        this.sorter.addPropertyChangeListener(new PropertyChangeListenerProxy("sorting", l));
1275
    }
1276
 
1277
    /**
1278
     * Whether this list is sorted by a column.
1279
     *
1280
     * @return true if this list is sorted.
1281
     */
1282
    public boolean isSorted() {
1283
        return this.sorter.isSorting();
1284
    }
1285
 
80 ilm 1286
    public final void setSortingEnabled(final boolean b) {
1287
        this.sorter.setSortingEnabled(b);
1288
    }
1289
 
1290
    public final boolean isSortingEnabled() {
1291
        return this.sorter.isSortingEnabled();
1292
    }
1293
 
17 ilm 1294
    private void fireSelectionId(int id, int selectedColumn) {
1295
        for (IListener l : this.listeners) {
1296
            l.selectionId(id, selectedColumn);
1297
        }
1298
    }
1299
 
1300
    protected final void fireNASelectionId() {
1301
        final int id = this.getSelectedId();
1302
        for (IListener l : this.naListeners) {
1303
            l.selectionId(id, -1);
1304
        }
1305
    }
1306
 
1307
    public final void addModelListener(final PropertyChangeListener l) {
1308
        this.supp.addPropertyChangeListener("model", l);
1309
    }
1310
 
1311
    public final void rmModelListener(final PropertyChangeListener l) {
1312
        this.supp.removePropertyChangeListener("model", l);
1313
    }
1314
 
1315
    /**
1316
     * Ensure that the passed listener will always listen on our current {@link #getModel() model}
1317
     * even if it changes. Warning: to signal model change
1318
     * {@link PropertyChangeListener#propertyChange(PropertyChangeEvent)} will be called with a
1319
     * <code>null</code> name.
1320
     *
1321
     * @param l the listener.
1322
     */
1323
    public final void addListenerOnModel(final PropertyChangeListener l) {
1324
        this.modelPCListeners.add(l);
1325
        if (getModel() != null)
1326
            getModel().addPropertyChangeListener(l);
1327
    }
1328
 
1329
    public final void rmListenerOnModel(final PropertyChangeListener l) {
1330
        this.modelPCListeners.remove(l);
1331
        if (getModel() != null)
1332
            getModel().rmPropertyChangeListener(l);
1333
    }
1334
 
80 ilm 1335
    /**
1336
     * Listen to the content of the selection, i.e. both selection ID change and data change of the
1337
     * current selection. Note: <code>l</code> is called for each selection change, even when
1338
     * {@link ListSelectionEvent#getValueIsAdjusting()} is <code>true</code>.
1339
     *
1340
     * @param l the listener.
1341
     */
1342
    public final void addSelectionDataListener(final PropertyChangeListener l) {
1343
        this.supp.addPropertyChangeListener(SELECTION_DATA_PROPNAME, l);
1344
    }
1345
 
1346
    public final void removeSelectionDataListener(final PropertyChangeListener l) {
1347
        this.supp.removePropertyChangeListener(SELECTION_DATA_PROPNAME, l);
1348
    }
1349
 
63 ilm 1350
    protected final void visibilityChanged() {
17 ilm 1351
        // test isDead() since in JComponent.removeNotify() first setDisplayable(false) (in super)
1352
        // then firePropertyChange("ancestor", null).
21 ilm 1353
        // thus we can still be visible while not displayable anymore
1354
        if (!this.isDead())
65 ilm 1355
            // we used to call isVisible() but that was incorrect : a component can be visible and
1356
            // not on screen. E.g. the frame would be made invisible, so this method was called but
1357
            // isVisible() hadn't changed (so still true) thus the model never slept (hence never
1358
            // hibernated, hence never was emptied).
1359
            this.getModel().setSleeping(!this.isShowing());
17 ilm 1360
    }
1361
 
1362
    /**
1363
     * The {@link ITableModel} of this list.
1364
     *
1365
     * @return the model, <code>null</code> if destroyed.
1366
     */
1367
    public ITableModel getModel() {
1368
        return (ITableModel) this.getTableModel();
1369
    }
1370
 
1371
    public TableModel getTableModel() {
93 ilm 1372
        assert SwingUtilities.isEventDispatchThread();
17 ilm 1373
        return this.sorter.getTableModel();
1374
    }
1375
 
1376
    private final void setTableModel(ITableModel t) {
1377
        final ITableModel old = this.getModel();
1378
        if (t == old)
1379
            return;
1380
 
1381
        if (old != null) {
1382
            for (final PropertyChangeListener l : this.modelPCListeners)
1383
                old.rmPropertyChangeListener(l);
80 ilm 1384
            old.removeTableModelListener(this.selectionDataListener);
17 ilm 1385
            if (this.hasRequest())
1386
                this.getRequest().rmWhereListener(this.filterListener);
1387
        }
1388
        this.sorter.setTableModel(t);
1389
        if (t != null) {
151 ilm 1390
            updateModelEditable();
17 ilm 1391
            // no need to listen to source columns since our ITableModel does, then it
1392
            // fireTableStructureChanged() and our JTable createDefaultColumnsFromModel() so
1393
            // columnAdded() and thus updateCols() are called. Note: we might want to listen to
1394
            // SQLTableModelColumn themselves (and not their list), e.g. if their renderers change.
1395
            for (final PropertyChangeListener l : this.modelPCListeners) {
1396
                t.addPropertyChangeListener(l);
1397
                // signal to the listeners that the model has changed (ie all of its properties)
1398
                l.propertyChange(new PropertyChangeEvent(t, null, null, null));
1399
            }
80 ilm 1400
            // listen to the SQL model and not this.sorter since change in sorting doesn't change
1401
            // the selection nor its data. Full listener since not all values are displayed.
1402
            t.addTableModelListener(this.selectionDataListener, true);
17 ilm 1403
            if (this.hasRequest()) {
1404
                this.getRequest().addWhereListener(this.filterListener);
1405
                // the where might have changed since we last listened
1406
                this.filterListener.propertyChange(null);
1407
            }
1408
        }
1409
        this.supp.firePropertyChange("model", old, t);
1410
    }
1411
 
1412
    // must be called when columnModel or getSource() changes
1413
    private void updateCols(final int index) {
1414
        final TableColumnModel columnModel = this.jTable.getColumnModel();
1415
        final int start = index < 0 ? 0 : index;
1416
        final int stop = index < 0 ? columnModel.getColumnCount() : index + 1;
1417
        for (int i = start; i < stop; i++) {
1418
            final TableColumn col = columnModel.getColumn(i);
93 ilm 1419
            final SQLTableModelColumn srcCol = this.getSource().getColumn(col.getModelIndex());
17 ilm 1420
            srcCol.install(col);
93 ilm 1421
            col.setIdentifier(srcCol.getIdentifier());
17 ilm 1422
            if (FORCE_ALT_CELL_RENDERER)
1423
                AlternateTableCellRenderer.setRendererAndListen(col);
1424
            else
1425
                AlternateTableCellRenderer.setRenderer(col);
1426
        }
1427
    }
1428
 
1429
    public final boolean hasRequest() {
1430
        return this.getSource() instanceof SQLTableModelSourceOnline;
1431
    }
1432
 
1433
    public final ListSQLRequest getRequest() {
142 ilm 1434
        return this.getSource().getReq();
17 ilm 1435
    }
1436
 
1437
    public final void setSource(SQLTableModelSource src) {
1438
        if (src == null)
1439
            throw new NullPointerException();
142 ilm 1440
        synchronized (this) {
1441
            // necessary to limit table model changes, since it recreates columns (and thus forget
1442
            // about customizations, eg renderers)
1443
            if (this.src == src)
1444
                return;
17 ilm 1445
 
142 ilm 1446
            this.src = src;
1447
        }
17 ilm 1448
        this.setTableModel(new ITableModel(src));
1449
    }
1450
 
142 ilm 1451
    public synchronized final SQLTableModelSource getSource() {
1452
        return this.src;
17 ilm 1453
    }
1454
 
1455
    public final File getConfigFile() {
1456
        // can be null if this is called before the end of the constructor
1457
        return this.tableStateManager == null ? null : this.tableStateManager.getConfigFile();
1458
    }
1459
 
65 ilm 1460
    public final void setConfigFile(File configFile) {
1461
        if (Boolean.getBoolean(STATELESS_TABLE_PROP))
1462
            configFile = null;
17 ilm 1463
        final File oldFile = this.getConfigFile();
1464
        if (!CompareUtils.equals(oldFile, configFile)) {
1465
            if (configFile == null)
1466
                this.tableStateManager.endAutoSave();
1467
            this.tableStateManager.setConfigFile(configFile);
1468
            if (oldFile == null)
1469
                this.tableStateManager.beginAutoSave();
1470
            loadTableState();
1471
        }
1472
    }
1473
 
93 ilm 1474
    private boolean loadTableState() {
73 ilm 1475
        // - if configFile changes setConfigFile() calls us
1476
        // - if the model changes, fireTableStructureChanged() is called and thus
1477
        // JTable.createDefaultColumnsFromModel() which calls us
1478
        if (this.getConfigFile() != null && this.getModel() != null)
93 ilm 1479
            return this.tableStateManager.loadState();
1480
        else
1481
            return false;
17 ilm 1482
    }
1483
 
1484
    /**
1485
     * Allow this list to be garbage collected. This method is necessary since this instance is
1486
     * listener of SQLTable which will never be gc'd.
1487
     */
1488
    private final void dispChanged() {
21 ilm 1489
        final boolean requiredToLive = this.isDisplayable() || this.retainCount > 0;
1490
        if (!requiredToLive && !this.isDead()) {
17 ilm 1491
            this.setTableModel(null);
21 ilm 1492
        } else if (requiredToLive && this.isDead()) {
142 ilm 1493
            this.setTableModel(new ITableModel(this.getSource()));
17 ilm 1494
        }
1495
    }
1496
 
21 ilm 1497
    /**
1498
     * Allow this to stay alive even if undisplayable. Attention, you must call {@link #release()}
1499
     * for each {@link #retain()} otherwise this instance will never be garbage collected.
1500
     */
1501
    public final void retain() {
1502
        this.retainCount++;
1503
        this.dispChanged();
1504
    }
1505
 
1506
    public final void release() {
1507
        if (this.retainCount == 0)
1508
            throw new IllegalStateException("Unbalanced release");
1509
        this.retainCount--;
1510
        this.dispChanged();
1511
    }
1512
 
17 ilm 1513
    public JTable getJTable() {
1514
        return this.jTable;
1515
    }
1516
 
151 ilm 1517
    private void updateModelEditable() {
1518
        final ITableModel m = this.getModel();
1519
        if (m != null) {
1520
            m.setCellsEditable(this.isCellModificationAllowed() && !getSource().getElem().isPrivate());
1521
            m.setOrderEditable(this.isOrderModificationAllowed() && (!getSource().getElem().isPrivate()));
1522
        }
1523
    }
1524
 
1525
    public void setModificationAllowed(boolean b) {
1526
        this.setCellModificationAllowed(b);
1527
        this.setOrderModificationAllowed(b);
1528
    }
1529
 
1530
    public void setCellModificationAllowed(boolean b) {
1531
        this.cellModificationAllowed = b;
1532
        updateModelEditable();
1533
    }
1534
 
1535
    public boolean isCellModificationAllowed() {
1536
        return this.cellModificationAllowed;
1537
    }
1538
 
1539
    public void setOrderModificationAllowed(boolean b) {
1540
        this.orderModificationAllowed = b;
1541
        updateModelEditable();
1542
    }
1543
 
1544
    public boolean isOrderModificationAllowed() {
1545
        return this.orderModificationAllowed;
1546
    }
1547
 
1548
    @Override
17 ilm 1549
    public void grabFocus() {
1550
        this.jTable.grabFocus();
1551
    }
1552
 
1553
    // *** workers
1554
 
1555
    private abstract class FilterWorker extends SwingWorker<String, Object> {
1556
 
1557
        @Override
1558
        protected final void done() {
1559
            if (!this.isCancelled()) {
1560
                // if doInBackground() wasn't cancelled, display our result
1561
                try {
1562
                    setFilter(this.get());
1563
                } catch (Exception e) {
1564
                    if (e instanceof ExecutionException && ((ExecutionException) e).getCause() instanceof InterruptedException) {
1565
                        final String msg = this.getClass() + " interruped";
1566
                        Log.get().fine(msg);
1567
                        setFilter(msg);
1568
                    } else {
1569
                        e.printStackTrace();
1570
                        setFilter(e.getLocalizedMessage());
1571
                    }
1572
                }
1573
                synchronized (IListe.this) {
1574
                    // only doInBackground() can be cancelled, so this might have received cancel()
1575
                    // after doInBackground() had completed but before done() had been called
1576
                    // thus filterWorker is not always this instance
1577
                    if (IListe.this.filterWorker == this) {
1578
                        IListe.this.filterWorker = null;
1579
                    }
1580
                }
1581
            }
1582
        }
1583
 
1584
    }
1585
 
1586
    private final class WhereFilterWorker extends FilterWorker {
1587
        private final Where w;
1588
 
1589
        private WhereFilterWorker(Where r) {
1590
            this.w = r;
1591
        }
1592
 
1593
        @Override
1594
        protected String doInBackground() throws InterruptedException {
1595
            return this.w == null ? "No where" : this.w.getClause();
1596
        }
1597
 
1598
    }
1599
 
1600
    private final class RowFilterWorker extends FilterWorker {
1601
        private final Collection<SQLRow> rows;
1602
 
1603
        private RowFilterWorker(Collection<SQLRow> r) {
1604
            this.rows = r;
1605
        }
1606
 
1607
        @Override
1608
        protected String doInBackground() throws InterruptedException {
1609
            if (this.getRows() == null)
1610
                return null;
1611
 
1612
            // attend 1 peu avant de faire des requetes, comme ca si le filtre change
1613
            // tout le temps, on ne commence meme pas (sleep jette InterruptedExn)
1614
            Thread.sleep(60);
1615
 
1616
            final List<String> ancestors = new ArrayList<String>();
142 ilm 1617
            final SQLElementDirectory dir = getSource().getElem().getDirectory();
17 ilm 1618
            // always put the description of getRows(), but only put their ancestor if they all have
1619
            // the same parent
1620
            Tuple2<SQLRow, String> parentAndDesc = getParent(this.getRows(), dir);
1621
            ancestors.add(parentAndDesc.get1());
1622
            SQLRow current = parentAndDesc.get0();
1623
            while (current != null) {
1624
                if (Thread.interrupted()) {
1625
                    throw new InterruptedException();
1626
                }
1627
                final SQLElement elem = dir.getElement(current.getTable());
1628
                ancestors.add(0, elem.getDescription(current));
67 ilm 1629
                current = elem.getForeignParent(current);
17 ilm 1630
            }
1631
 
1632
            return CollectionUtils.join(ancestors, SEP);
1633
        }
1634
 
1635
        private Tuple2<SQLRow, String> getParent(Collection<SQLRow> rows, final SQLElementDirectory dir) throws InterruptedException {
1636
            SQLRow parent = null;
1637
            boolean sameParent = true;
1638
            final List<String> desc = new ArrayList<String>(rows.size());
1639
 
1640
            for (final SQLRow current : rows) {
1641
                if (Thread.interrupted()) {
1642
                    throw new InterruptedException();
1643
                }
1644
                final SQLElement elem = dir.getElement(current.getTable());
1645
                if (parent == null || sameParent) {
67 ilm 1646
                    final SQLRow currentParent = elem.getForeignParent(current);
17 ilm 1647
                    if (parent == null)
1648
                        parent = currentParent;
1649
                    else if (!parent.equals(currentParent))
1650
                        sameParent = false;
1651
                }
1652
                desc.add(elem.getDescription(current));
1653
            }
1654
 
1655
            return Tuple2.create(sameParent ? parent : null, CollectionUtils.join(desc, " ●"));
1656
        }
1657
 
1658
        private final Collection<SQLRow> getRows() {
1659
            return this.rows;
1660
        }
1661
 
1662
        @Override
1663
        public String toString() {
1664
            return super.toString() + " on " + this.getRows();
1665
        }
1666
    }
1667
}