OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 21 | Rev 63 | 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
 /*
15
 * Created on 23 janv. 2005
16
 */
17
package org.openconcerto.ui.component;
18
 
19
import static org.openconcerto.ui.component.ComboLockedMode.LOCKED;
20
import static org.openconcerto.ui.component.ComboLockedMode.UNLOCKED;
21
import org.openconcerto.ui.component.text.TextComponent;
22
import org.openconcerto.ui.valuewrapper.ValueChangeSupport;
23
import org.openconcerto.ui.valuewrapper.ValueWrapper;
24
import org.openconcerto.utils.CompareUtils;
25
import org.openconcerto.utils.checks.ValidListener;
21 ilm 26
import org.openconcerto.utils.checks.ValidState;
17 ilm 27
import org.openconcerto.utils.model.ListComboBoxModel;
28
import org.openconcerto.utils.text.DocumentFilterList;
29
import org.openconcerto.utils.text.SimpleDocumentFilter;
30
import org.openconcerto.utils.text.SimpleDocumentListener;
31
 
32
import java.awt.Button;
33
import java.awt.Component;
34
import java.awt.Dimension;
35
import java.awt.event.ActionEvent;
36
import java.awt.event.ActionListener;
37
import java.awt.event.KeyAdapter;
38
import java.awt.event.KeyEvent;
39
import java.beans.PropertyChangeListener;
40
import java.util.List;
41
 
42
import javax.swing.ComboBoxEditor;
43
import javax.swing.JButton;
44
import javax.swing.JComboBox;
45
import javax.swing.JComponent;
46
import javax.swing.SwingWorker;
47
import javax.swing.event.DocumentEvent;
48
import javax.swing.text.AbstractDocument;
49
import javax.swing.text.BadLocationException;
19 ilm 50
import javax.swing.text.DocumentFilter.FilterBypass;
17 ilm 51
import javax.swing.text.JTextComponent;
52
 
53
/**
54
 * A comboBox that can be editable or not, and whose values are taken from a ITextComboCache.
55
 *
56
 * @author Sylvain CUAZ
57
 */
58
public class ITextCombo extends JComboBox implements ValueWrapper<String>, TextComponent {
59
 
60
    private static final String DEFAULTVALUE = "";
61
 
62
    private final String defaultValue;
63
    private final ComboLockedMode locked;
25 ilm 64
    private final ValueChangeSupport<String> supp;
17 ilm 65
    protected final boolean autoComplete;
66
    protected boolean keyPressed;
67
    private boolean completing;
68
 
69
    // cache
70
    private boolean cacheLoading;
71
    private String objToSelect;
72
    private boolean modeToSet;
73
    protected boolean modifyingDoc;
74
 
75
    private ITextComboCache cache;
76
 
77
    public ITextCombo() {
78
        this(DEFAULTVALUE);
79
    }
80
 
81
    public ITextCombo(String defaultValue) {
82
        this(defaultValue, UNLOCKED);
83
    }
84
 
85
    public ITextCombo(boolean locked) {
86
        this(locked ? LOCKED : UNLOCKED);
87
    }
88
 
89
    public ITextCombo(ComboLockedMode mode) {
90
        this(DEFAULTVALUE, mode);
91
    }
92
 
93
    public ITextCombo(String defaultValue, ComboLockedMode mode) {
94
        super(new ListComboBoxModel());
95
        // messes with our checkCache
96
        this.getListModel().setSelectOnAdd(false);
97
        this.supp = new ValueChangeSupport<String>(this);
98
        this.locked = mode;
99
 
100
        this.defaultValue = defaultValue;
101
 
102
        this.autoComplete = true;
103
        this.keyPressed = false;
104
        this.completing = false;
105
 
106
        this.cache = null;
107
        this.cacheLoading = false;
108
        this.modifyingDoc = false;
109
 
110
        this.setMinimumSize(new Dimension(80, 22));
111
        // Test de Preferred Size pour ne pas exploser les GridBagLayouts
112
        this.setPreferredSize(new Dimension(120, 22));
113
        this.objToSelect = defaultValue;
114
        // argument is ignored
115
        this.setEditable(true);
116
 
117
        // ATTN marche car locked est final, sinon il faudrait pouvoir enlever/ajouter les listeners
118
        if (this.isLocked()) {
119
            this.addActionListener(new ActionListener() {
120
                public void actionPerformed(ActionEvent e) {
121
                    ITextCombo.this.supp.fireValueChange();
122
                }
123
            });
124
        } else {
125
            // pour écouter quand notre contenu change
126
            // marche à la fois pour edition du texte et la sélection d'un élément
127
            this.getTextComp().getDocument().addDocumentListener(new SimpleDocumentListener() {
128
                public void update(DocumentEvent e) {
129
                    // if we are responsible for this event, ignore it
130
                    if (!ITextCombo.this.modifyingDoc)
131
                        setValue(getTextComp().getText());
132
                    ITextCombo.this.supp.fireValueChange();
133
                }
134
            });
135
        }
136
 
137
        if (Boolean.getBoolean("org.openconcerto.ui.simpleTraversal"))
138
            for (final Component child : this.getComponents()) {
139
                if (child instanceof JButton || child instanceof Button)
140
                    child.setFocusable(false);
141
            }
142
    }
143
 
144
    public void configureEditor(ComboBoxEditor anEditor, Object anItem) {
145
        // quand on quitte une combo, elle fait setSelectedItem(), qui appelle editor.setItem()
146
        // qui fait editor.getComponent().setText(), quit fait un removeAll() suivi d'un addAll()
147
        // donc emptyChange(true) puis emptyChange(false).
148
        // Ce qui fait que quand on quitte une combo required pour cliquer sur "ajouter", le bouton
149
        // flashe (il passe brièvement en grisé) et on ne peut ajouter.
150
        if (!anEditor.getItem().equals(anItem))
151
            super.configureEditor(anEditor, anItem);
152
    }
153
 
154
    protected final ComboLockedMode getMode() {
155
        return this.locked;
156
    }
157
 
158
    private boolean isLocked() {
159
        return this.locked == LOCKED;
160
    }
161
 
162
    public void initCache(ITextComboCache cache) {
163
        if (this.cache != null)
164
            throw new IllegalStateException("cache already set " + this.cache);
165
 
166
        this.cache = cache;
167
 
168
        new MutableListComboPopupListener(new MutableListCombo() {
169
            public ComboLockedMode getMode() {
170
                return ITextCombo.this.getMode();
171
            }
172
 
173
            public Component getPopupComp() {
174
                return getEditor().getEditorComponent();
175
            }
176
 
177
            public void addCurrentText() {
178
                ITextCombo.this.addCurrentText();
179
            }
180
 
181
            public void removeCurrentText() {
182
                ITextCombo.this.removeCurrentText();
183
            }
25 ilm 184
 
185
            @Override
186
            public boolean canReload() {
187
                return true;
188
            }
189
 
190
            @Override
191
            public void reload() {
192
                ITextCombo.this.loadCache(true);
193
            }
17 ilm 194
        }).listen();
195
 
25 ilm 196
        this.loadCache(false);
17 ilm 197
 
198
        // ATTN marche car locked est final
199
        if (!this.isLocked()) {
200
            this.getTextComp().addKeyListener(new KeyAdapter() {
201
                @Override
202
                public void keyTyped(KeyEvent e) {
203
                    // not keyPressed() else we activate the completion as soon as any key is
204
                    // pressed (even ctrl)
205
                    ITextCombo.this.keyPressed = true;
206
                }
207
 
208
                @Override
209
                public void keyReleased(KeyEvent e) {
210
                    ITextCombo.this.keyPressed = false;
211
                }
212
            });
213
            DocumentFilterList.add((AbstractDocument) this.getTextComp().getDocument(), new SimpleDocumentFilter() {
214
                @Override
215
                protected boolean change(FilterBypass fb, String newText, Mode mode) throws BadLocationException {
216
                    // do not complete a remove (otherwise impossible to remove the last char for
217
                    // example), only complete when the user is typing (eg a key is pressed)
218
                    // otherwise just setting the value to something that can be completed changes
219
                    // it.
220
                    if (mode != Mode.REMOVE && ITextCombo.this.autoComplete && ITextCombo.this.keyPressed)
221
                        return complete(fb, newText);
222
                    else
223
                        return true;
224
                }
225
            });
226
        }
227
    }
228
 
229
    protected final boolean complete(FilterBypass fb, final String originalText) throws BadLocationException {
230
        // no need to check the cache since we only use the combo items
231
        // and they only are modified by the EDT, our executing thread too
232
        boolean res = true;
233
        if (!this.completing) {
234
            this.completing = true;
235
            // ne completer que si le texte fait plus de 2 char et n'est pas que des chiffres
236
            if (originalText.length() > 2 && !originalText.matches("^\\d*$")) {
237
                String completion = this.getCompletion(originalText);
238
                if (completion != null && !originalText.trim().equalsIgnoreCase(completion.trim())) {
239
                    fb.replace(0, fb.getDocument().getLength(), completion, null);
240
                    // we handled the modification
241
                    res = false;
242
                    this.getTextComp().setSelectionStart(originalText.length());
243
                    this.getTextComp().setSelectionEnd(completion.length());
244
                }
245
            }
246
            this.completing = false;
247
        }
248
        return res;
249
    }
250
 
251
    /**
252
     * Recherche si on peut completer la string avec les items de completion
253
     *
254
     * @param string the start
255
     * @return <code>null</code> si pas trouve, sinon le mot complet
256
     */
257
    private String getCompletion(String string) {
258
        if (string.length() < 1) {
259
            return null;
260
        }
261
 
262
        int count = 0;
263
        String result = null;
264
        for (final Object obj : this.getListModel().getList()) {
265
            final String item = (String) obj;
266
            if (item.startsWith(string)) {
267
                count++;
268
                result = item;
269
            }
270
        }
271
        if (count == 1)
272
            return result;
273
        else
274
            return null;
275
    }
276
 
277
    private ListComboBoxModel getListModel() {
278
        return (ListComboBoxModel) this.getModel();
279
    }
280
 
281
    public void setEditable(boolean b) {
282
        // ne pas faire setEditable(false), sinon plus de textField
283
        super.setEditable(!isLocked());
284
    }
285
 
286
    @Override
287
    public void setEnabled(boolean b) {
288
        if (this.cacheLoading)
289
            this.modeToSet = b;
290
        else {
291
            super.setEnabled(b);
292
        }
293
    }
294
 
295
    // *** cache
296
 
297
    // charge les elements de completion si besoin
25 ilm 298
    private synchronized final void loadCache(final boolean force) {
17 ilm 299
        if (!this.cacheLoading) {
300
            this.modeToSet = this.isEnabled();
301
            this.setEnabled(false);
25 ilm 302
            this.objToSelect = this.getValue();
17 ilm 303
            this.cacheLoading = true;
25 ilm 304
            final SwingWorker<List<String>, Object> sw = new SwingWorker<List<String>, Object>() {
17 ilm 305
                @Override
306
                protected List<String> doInBackground() throws Exception {
25 ilm 307
                    return force ? ITextCombo.this.cache.loadCache(false) : ITextCombo.this.cache.getCache();
17 ilm 308
                }
309
 
310
                @Override
311
                protected void done() {
312
                    synchronized (this) {
313
                        ITextCombo.this.modifyingDoc = true;
314
                    }
315
                    getListModel().removeAllElements();
316
                    try {
317
                        getListModel().addAll(this.get());
318
                    } catch (Exception e) {
319
                        e.printStackTrace();
320
                        getListModel().addElement(e.getLocalizedMessage());
321
                    }
322
                    synchronized (this) {
323
                        ITextCombo.this.modifyingDoc = false;
324
                        ITextCombo.this.cacheLoading = false;
325
                    }
326
                    // otherwise getSelectedItem() always returns null
327
                    if (isLocked() && getModel().getSize() == 0)
328
                        throw new IllegalStateException(ITextCombo.this + " locked but no items.");
329
                    // restaurer l'état
330
                    setEnabled(ITextCombo.this.modeToSet);
331
                    setValue(ITextCombo.this.objToSelect);
332
                }
333
            };
334
            sw.execute();
335
        }
336
    }
337
 
338
    private final Object makeObj(final String item) {
339
        return item;
340
        // see #addItem ; not necessary since there's never any duplicates
341
    }
342
 
343
    /**
344
     * Add <code>s</code> to the list if it's not empty and not already present.
345
     *
346
     * @param s the string to be added, can be <code>null</code>.
347
     * @return <code>true</code> if s is really added.
348
     */
349
    private final boolean addToCache(String s) {
350
        if (s != null && s.length() > 0 && this.getListModel().getList().indexOf(s) < 0) {
351
            this.addItem(makeObj(s));
352
            return true;
353
        } else
354
            return false;
355
    }
356
 
357
    private final void removeCurrentText() {
358
        final String t = this.getTextComp().getText();
359
        this.cache.deleteFromCache(t);
360
        for (int i = 0; i < this.getItemCount(); i++) {
361
            final String o = (String) this.getItemAt(i);
362
            if (o.equals(t)) {
363
                this.removeItemAt(i);
364
                break;
365
            }
366
        }
367
    }
368
 
369
    private final void addCurrentText() {
370
        final String t = this.getTextComp().getText();
371
        if (this.addToCache(t)) {
372
            this.cache.addToCache(t);
373
        }
374
    }
375
 
376
    // *** value
377
 
378
    public void addValueListener(PropertyChangeListener l) {
379
        this.supp.addValueListener(l);
380
    }
381
 
382
    public void rmValueListener(PropertyChangeListener l) {
383
        this.supp.rmValueListener(l);
384
    }
385
 
386
    synchronized public final void setValue(String val) {
387
        if (this.cacheLoading)
388
            this.objToSelect = val;
389
        else if (!CompareUtils.equals(this.getSelectedItem(), val)) {
390
            // complete only user input, not programmatic
391
            this.completing = true;
392
            this.setSelectedItem(makeObj(val));
393
            this.completing = false;
394
        }
395
    }
396
 
397
    public void resetValue() {
398
        this.setValue(this.defaultValue);
399
    }
400
 
401
    public String getValue() {
402
        // this.getSelectedItem() renvoie vide quand on tape du texte sans sélection
403
        return (String) (this.isLocked() ? this.getSelectedItem() : this.getEditor().getItem());
404
    }
405
 
406
    public JComponent getComp() {
407
        return this;
408
    }
409
 
21 ilm 410
    @Override
411
    public ValidState getValidState() {
17 ilm 412
        // string toujours valide
21 ilm 413
        return ValidState.getTrueInstance();
17 ilm 414
    }
415
 
21 ilm 416
    @Override
17 ilm 417
    public void addValidListener(ValidListener l) {
418
        // nothing to do
419
    }
420
 
19 ilm 421
    @Override
422
    public void removeValidListener(ValidListener l) {
423
        // nothing to do
424
    }
425
 
17 ilm 426
    // document
427
 
428
    public JTextComponent getTextComp() {
429
        if (this.isLocked())
430
            return null;
431
        else
432
            return (JTextComponent) this.getEditor().getEditorComponent();
433
    }
434
 
435
    @Override
436
    public String toString() {
437
        return this.getClass().getName() + " " + this.locked + " cache: " + this.cache;
438
    }
439
 
440
}