OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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