OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 21 | Rev 26 | 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
 *
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.sqlobject;
15
 
16
import org.openconcerto.sql.Configuration;
17
import org.openconcerto.sql.model.SQLRow;
18
import org.openconcerto.sql.model.SQLRowAccessor;
19
import org.openconcerto.sql.model.SQLTable;
20
import org.openconcerto.sql.request.ComboSQLRequest;
21
import org.openconcerto.sql.request.SQLForeignRowItemView;
22
import org.openconcerto.sql.request.SQLRowItemView;
23
import org.openconcerto.sql.sqlobject.itemview.RowItemViewComponent;
24
import org.openconcerto.sql.view.search.SearchSpec;
25
import org.openconcerto.ui.FontUtils;
26
import org.openconcerto.ui.component.ComboLockedMode;
27
import org.openconcerto.ui.component.combo.ISearchableCombo;
28
import org.openconcerto.ui.component.text.TextComponent;
29
import org.openconcerto.ui.coreanimation.Pulseable;
30
import org.openconcerto.ui.valuewrapper.ValueChangeSupport;
31
import org.openconcerto.ui.valuewrapper.ValueWrapper;
32
import org.openconcerto.utils.cc.ITransformer;
33
import org.openconcerto.utils.checks.EmptyChangeSupport;
34
import org.openconcerto.utils.checks.EmptyListener;
35
import org.openconcerto.utils.checks.EmptyObj;
36
import org.openconcerto.utils.checks.ValidListener;
21 ilm 37
import org.openconcerto.utils.checks.ValidState;
17 ilm 38
import org.openconcerto.utils.model.DefaultIMutableListModel;
39
 
40
import java.awt.Component;
41
import java.awt.GridLayout;
25 ilm 42
import java.awt.event.ActionEvent;
17 ilm 43
import java.awt.event.HierarchyEvent;
44
import java.awt.event.HierarchyListener;
45
import java.beans.PropertyChangeEvent;
46
import java.beans.PropertyChangeListener;
47
import java.util.Arrays;
48
import java.util.List;
49
 
50
import javax.accessibility.Accessible;
25 ilm 51
import javax.swing.AbstractAction;
17 ilm 52
import javax.swing.Action;
53
import javax.swing.Icon;
54
import javax.swing.ImageIcon;
55
import javax.swing.JComponent;
56
import javax.swing.JPanel;
57
import javax.swing.SwingUtilities;
58
import javax.swing.event.AncestorEvent;
59
import javax.swing.event.AncestorListener;
60
import javax.swing.plaf.basic.ComboPopup;
61
import javax.swing.text.JTextComponent;
62
 
63
/**
64
 * A comboBox who lists items provided by a ComboSQLRequest. It listens to table changes, but can
65
 * also be reloaded by calling {@link #fillCombo()}. Search is also available , see
66
 * {@link #search(SearchSpec)}.
67
 *
68
 * @author Sylvain CUAZ
69
 * @see #uiInit(ComboSQLRequest)
70
 */
71
public class SQLRequestComboBox extends JPanel implements SQLForeignRowItemView, ValueWrapper<Integer>, EmptyObj, TextComponent, Pulseable, RowItemViewComponent {
72
 
73
    public static final String UNDEFINED_STRING = "----- ??? -----";
74
 
75
    public static enum ComboMode {
76
        /** The combo is completely disabled */
77
        DISABLED,
78
        /** The combo is disabled but the magnifying glass is enabled */
79
        ENABLED,
80
        /** The combo is fully functionnal (its default) */
81
        EDITABLE
82
    }
83
 
84
    // on l'a pas toujours à l'instanciation
85
    private IComboModel req;
86
 
87
    // Interface graphique
88
    protected final ISearchableCombo<IComboSelectionItem> combo;
89
 
90
    // supports
21 ilm 91
    private final ValueChangeSupport<Integer> supp;
17 ilm 92
    private final EmptyChangeSupport emptySupp;
93
 
94
    // le mode actuel
95
    private ComboMode mode;
96
    // le mode à sélectionner à la fin du updateAll
97
    private ComboMode modeToSelect;
98
 
99
    // to speed up the combo
100
    private final String stringStuff;
101
 
102
    public SQLRequestComboBox() {
103
        this(true);
104
    }
105
 
106
    public SQLRequestComboBox(boolean addUndefined) {
107
        this(addUndefined, -1);
108
    }
109
 
110
    public SQLRequestComboBox(boolean addUndefined, int preferredWidthInChar) {
111
        this.setOpaque(false);
112
        this.mode = ComboMode.EDITABLE;
113
        // necessary when uiInit() is called with a model already updating
114
        // (otherwise when it finishes modeToSelect will still be null)
115
        this.modeToSelect = this.mode;
116
        if (preferredWidthInChar > 0) {
117
            final char[] a = new char[preferredWidthInChar];
118
            Arrays.fill(a, ' ');
119
            this.stringStuff = String.valueOf(a);
120
        } else
121
            this.stringStuff = "123456789012345678901234567890";
122
 
123
        this.combo = new ISearchableCombo<IComboSelectionItem>(ComboLockedMode.LOCKED, 1, this.stringStuff.length());
124
        this.combo.setIncludeEmpty(addUndefined);
25 ilm 125
        this.combo.getActions().add(new AbstractAction("Recharger") {
126
            @Override
127
            public void actionPerformed(ActionEvent e) {
128
                // ignore cache since a user explicitly asked for an update
129
                fillCombo(null, false);
130
            }
131
        });
17 ilm 132
 
133
        this.emptySupp = new EmptyChangeSupport(this);
134
        this.supp = new ValueChangeSupport<Integer>(this);
135
    }
136
 
137
    /**
138
     * Specify which item will be selected the first time the combo is filled (unless setValue() is
139
     * called before the fill).
140
     *
141
     * @param firstFillTransf will be passed the items and should return the wanted selection.
142
     */
143
    public final void setFirstFillSelection(ITransformer<List<IComboSelectionItem>, IComboSelectionItem> firstFillTransf) {
144
        this.req.setFirstFillSelection(firstFillTransf);
145
    }
146
 
147
    @Override
148
    public void init(SQLRowItemView v) {
149
        final SQLTable foreignTable = v.getField().getDBSystemRoot().getGraph().getForeignTable(v.getField());
25 ilm 150
        if (!this.hasModel())
151
            this.uiInit(Configuration.getInstance().getDirectory().getElement(foreignTable).getComboRequest());
152
        else if (this.getRequest().getPrimaryTable() != foreignTable)
153
            throw new IllegalArgumentException("Tables are different " + getRequest().getPrimaryTable().getSQLName() + " != " + foreignTable.getSQLName());
17 ilm 154
    }
155
 
156
    /**
157
     * Init de l'interface graphique.
158
     *
159
     * @param req which table to display and how.
160
     */
161
    public final void uiInit(final ComboSQLRequest req) {
162
        this.uiInit(new IComboModel(req));
163
    }
164
 
21 ilm 165
    private boolean hasModel() {
166
        return this.req != null;
167
    }
168
 
17 ilm 169
    public final void uiInit(final IComboModel req) {
21 ilm 170
        if (hasModel())
17 ilm 171
            throw new IllegalStateException(this + " already inited.");
172
 
173
        this.req = req;
174
        // listeners
175
        this.req.addListener("updating", new PropertyChangeListener() {
176
            @Override
177
            public void propertyChange(PropertyChangeEvent evt) {
178
                updatingChanged((Boolean) evt.getNewValue());
179
            }
180
        });
181
        this.req.addValueListener(new PropertyChangeListener() {
182
            @Override
183
            public void propertyChange(PropertyChangeEvent evt) {
184
                modelValueChanged();
185
            }
186
        });
187
 
188
        // remove listeners to allow this to be gc'd
189
        this.addHierarchyListener(new HierarchyListener() {
190
            public void hierarchyChanged(HierarchyEvent e) {
191
                if ((e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0)
192
                    updateListeners();
193
            }
194
        });
195
        // initial state ; since we're in the EDT, the DISPLAYABILITY cannot change between
196
        // addHierarchyListener() and here
197
        updateListeners();
198
        this.addAncestorListener(new AncestorListener() {
199
 
200
            @Override
201
            public void ancestorAdded(AncestorEvent event) {
202
                SQLRequestComboBox.this.req.setOnScreen(true);
203
            }
204
 
205
            @Override
206
            public void ancestorRemoved(AncestorEvent event) {
207
                SQLRequestComboBox.this.req.setOnScreen(false);
208
            }
209
 
210
            @Override
211
            public void ancestorMoved(AncestorEvent event) {
212
                // don't care
213
            }
214
        });
215
 
216
        FontUtils.setFontFor(this.combo, "ComboBox", this.getRequest().getSeparatorsChars());
217
        // try to speed up that damned JList, as those fine Swing engineers put it "This is
218
        // currently hacky..."
219
        for (int i = 0; i < this.combo.getUI().getAccessibleChildrenCount(this.combo); i++) {
220
            final Accessible acc = this.combo.getUI().getAccessibleChild(this.combo, i);
221
            if (acc instanceof ComboPopup) {
222
                final ComboPopup cp = (ComboPopup) acc;
223
                cp.getList().setPrototypeCellValue(new IComboSelectionItem(-1, this.stringStuff));
224
            }
225
        }
226
 
227
        this.combo.setIconFactory(new ITransformer<IComboSelectionItem, Icon>() {
228
            @Override
229
            public Icon transformChecked(final IComboSelectionItem input) {
230
                return getIconFor(input);
231
            }
232
        });
233
        this.combo.initCache(this.req);
234
        this.combo.addValueListener(new PropertyChangeListener() {
235
            public void propertyChange(PropertyChangeEvent evt) {
236
                comboValueChanged();
237
            }
238
        });
239
        // synchronize with the state of req (after adding our value listener)
240
        this.modelValueChanged();
241
 
21 ilm 242
        // getValidSate() depends on this.req
243
        this.supp.fireValidChange();
244
 
17 ilm 245
        this.uiLayout();
246
 
247
        // *without* : resetValue() => doUpdateAll() since it was never filled
248
        // then this is made displayable => setRunning(true) => dirty = true since not on screen
249
        // finally made visible => setOnScreen(true) => second doUpdateAll()
250
        // *with* : setRunning(true) => update ignored since not on screen, dirty = true
251
        // resetValue() => doUpdateAll() since it was never filled, dirty = false
252
        // then this is made displayable => setRunning(true) => no change
253
        // finally made visible => setOnScreen(true) => not dirty => no update
254
        this.req.setRunning(true);
255
    }
256
 
257
    private final void updatingChanged(final Boolean newValue) {
258
        if (Boolean.TRUE.equals(newValue)) {
259
            this.modeToSelect = this.getEnabled();
260
            // ne pas interagir pendant le chargement
261
            this.setEnabled(ComboMode.DISABLED, true);
262
        } else {
263
            this.setEnabled(this.modeToSelect);
264
        }
265
    }
266
 
267
    public final List<Action> getActions() {
268
        return this.combo.getActions();
269
    }
270
 
271
    protected void updateListeners() {
21 ilm 272
        if (hasModel()) {
17 ilm 273
            this.req.setRunning(this.isDisplayable());
274
        }
275
    }
276
 
277
    public final ComboSQLRequest getRequest() {
278
        return this.req.getRequest();
279
    }
280
 
281
    protected void uiLayout() {
282
        this.setLayout(new GridLayout(1, 1));
283
        this.add(this.combo);
284
    }
285
 
286
    public void setDebug(boolean trace) {
287
        this.req.setDebug(trace);
288
        this.combo.setDebug(trace);
289
    }
290
 
291
    /**
292
     * Whether this combo is allowed to delay {@link #fillCombo()} when it isn't visible.
293
     *
294
     * @param sleepAllowed <code>true</code> if reloads can be delayed.
295
     */
296
    public final void setSleepAllowed(boolean sleepAllowed) {
297
        this.req.setSleepAllowed(sleepAllowed);
298
    }
299
 
300
    public final boolean isSleepAllowed() {
301
        return this.req.isSleepAllowed();
302
    }
303
 
304
    /**
305
     * Reload this combo. This method is thread-safe.
306
     */
307
    public synchronized final void fillCombo() {
308
        this.fillCombo(null);
309
    }
310
 
311
    public synchronized final void fillCombo(final Runnable r) {
25 ilm 312
        this.fillCombo(r, true);
17 ilm 313
    }
314
 
25 ilm 315
    public synchronized final void fillCombo(final Runnable r, final boolean readCache) {
316
        this.req.fillCombo(r, readCache);
317
    }
318
 
17 ilm 319
    // combo
320
 
321
    public final List<IComboSelectionItem> getItems() {
322
        return this.getComboModel().getList();
323
    }
324
 
325
    private DefaultIMutableListModel<IComboSelectionItem> getComboModel() {
326
        return (DefaultIMutableListModel<IComboSelectionItem>) this.combo.getCache();
327
    }
328
 
329
    public final IComboSelectionItem getItem(int id) {
330
        return this.req.getItem(id);
331
    }
332
 
333
    // *** value
334
 
335
    public final void resetValue() {
336
        this.setValue((Integer) null);
337
    }
338
 
339
    public final void setValue(int id) {
340
        this.req.setValue(id);
341
    }
342
 
343
    public final void setValue(Integer id) {
344
        if (id == null)
345
            this.setValue(SQLRow.NONEXISTANT_ID);
346
        else
347
            this.setValue((int) id);
348
    }
349
 
350
    public final void setValue(SQLRowAccessor r) {
351
        this.setValue(r == null ? null : r.getID());
352
    }
353
 
354
    public final Integer getValue() {
355
        final IComboSelectionItem o = this.req.getSelectedItem();
356
        if (o != null && o.getId() >= SQLRow.MIN_VALID_ID)
357
            return o.getId();
358
        else {
359
            return null;
360
        }
361
    }
362
 
363
    /**
364
     * Renvoie l'ID de l'item sélectionné.
365
     *
366
     * @return l'ID de l'item sélectionné, <code>SQLRow.NONEXISTANT_ID</code> si combo vide.
367
     */
368
    public final int getSelectedId() {
369
        return this.req.getSelectedId();
370
    }
371
 
372
    /**
373
     * The selected row or <code>null</code> if this is empty.
374
     *
375
     * @return a SQLRow (non fetched) or <code>null</code>.
376
     */
377
    public final SQLRow getSelectedRow() {
378
        if (this.isEmpty())
379
            return null;
380
        else {
381
            return this.req.getSelectedRow();
382
        }
383
    }
384
 
385
    private void modelValueChanged() {
386
        final IComboSelectionItem newValue = this.req.getValue();
387
        // user makes invalid edit => combo invalid=true and value=null => model value=null
388
        // and if we call combo.setValue() it will change invalid to false
389
        if (this.combo.getValue() != newValue)
390
            this.combo.setValue(newValue);
391
    }
392
 
393
    private final void comboValueChanged() {
394
        this.req.setValue(this.combo.getValue());
395
        this.supp.fireValueChange();
396
        this.emptySupp.fireEmptyChange(this.isEmpty());
397
    }
398
 
399
    /**
400
     * Whether missing item are fetched from the database. If {@link #setValue(Integer)} is called
401
     * with an ID not present in the list and addMissingItem is <code>true</code> then that ID will
402
     * be fetched and added to the list, if it is <code>false</code> the selection will be cleared.
403
     *
404
     * @return <code>true</code> if missing item are fetched.
405
     */
406
    public final boolean addMissingItem() {
407
        return this.req.addMissingItem();
408
    }
409
 
410
    public final void setAddMissingItem(boolean addMissingItem) {
411
        this.req.setAddMissingItem(addMissingItem);
412
    }
413
 
414
    public final void setEditable(boolean b) {
415
        this.setEnabled(b ? ComboMode.EDITABLE : ComboMode.ENABLED);
416
    }
417
 
418
    public final void setEnabled(boolean b) {
419
        // FIXME add mode to RIV
420
        this.setEnabled(b ? ComboMode.EDITABLE : ComboMode.ENABLED);
421
    }
422
 
423
    public final void setEnabled(ComboMode mode) {
424
        this.setEnabled(mode, false);
425
    }
426
 
427
    private final void setEnabled(ComboMode mode, boolean priv) {
428
        assert SwingUtilities.isEventDispatchThread();
429
        if (!priv && this.isUpdating()) {
430
            this.modeToSelect = mode;
431
        } else {
432
            this.mode = mode;
433
            modeChanged(mode);
434
        }
435
    }
436
 
437
    protected void modeChanged(ComboMode mode) {
438
        this.combo.setEnabled(mode == ComboMode.EDITABLE);
439
    }
440
 
441
    public final ComboMode getEnabled() {
442
        return this.mode;
443
    }
444
 
445
    public String toString() {
446
        return this.getClass().getName() + " " + this.req;
447
    }
448
 
449
    public final boolean isEmpty() {
450
        return this.req == null || this.req.isEmpty();
451
    }
452
 
453
    public final void addEmptyListener(EmptyListener l) {
454
        this.emptySupp.addEmptyListener(l);
455
    }
456
 
457
    public final void addValueListener(PropertyChangeListener l) {
458
        this.supp.addValueListener(l);
459
    }
460
 
461
    public final void rmValueListener(PropertyChangeListener l) {
462
        this.supp.rmValueListener(l);
463
    }
464
 
465
    public final void addItemsListener(PropertyChangeListener l) {
466
        this.addItemsListener(l, false);
467
    }
468
 
469
    /**
470
     * Adds a listener on the items of this combo.
471
     *
472
     * @param l the listener.
473
     * @param all <code>true</code> if <code>l</code> should be called for all changes, including UI
474
     *        ones (e.g. adding a '-- loading --' item).
475
     */
476
    public final void addItemsListener(PropertyChangeListener l, final boolean all) {
477
        this.req.addItemsListener(l, all);
478
    }
479
 
480
    public final void rmItemsListener(PropertyChangeListener l) {
481
        this.req.rmItemsListener(l);
482
    }
483
 
21 ilm 484
    @Override
485
    public ValidState getValidState() {
486
        // OK, since we fire every time the combo does (see our ctor)
487
        // we are valid if we can return a value and getValue() needs this.req
488
        return ValidState.getNoReasonInstance(hasModel()).and(this.combo.getValidState());
17 ilm 489
    }
490
 
21 ilm 491
    @Override
17 ilm 492
    public final void addValidListener(ValidListener l) {
493
        this.supp.addValidListener(l);
494
    }
495
 
19 ilm 496
    @Override
497
    public void removeValidListener(ValidListener l) {
498
        this.supp.removeValidListener(l);
499
    }
500
 
17 ilm 501
    private Icon getIconFor(IComboSelectionItem value) {
502
        final Icon i;
503
        if (value == null) {
504
            // happens when the combo is empty
505
            i = null;
506
        } else {
507
            final int flag = value.getFlag();
508
            if (flag == IComboSelectionItem.WARNING_FLAG)
509
                i = new ImageIcon(this.getClass().getResource("warning.png"));
510
            else if (flag == IComboSelectionItem.ERROR_FLAG)
511
                i = new ImageIcon(this.getClass().getResource("error.png"));
512
            else
513
                i = null;
514
        }
515
        return i;
516
    }
517
 
518
    public final JComponent getComp() {
519
        return this;
520
    }
521
 
522
    public JTextComponent getTextComp() {
523
        return this.combo.getTextComp();
524
    }
525
 
526
    public Component getPulseComponent() {
527
        return this.combo;
528
    }
529
 
530
    // *** search
531
 
532
    public final void search(SearchSpec spec) {
533
        this.req.search(spec);
534
    }
535
 
536
    public final boolean isUpdating() {
537
        return this.req.isUpdating();
538
    }
539
}