OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 19 | Rev 41 | 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.model.SQLField;
17
import org.openconcerto.sql.model.SQLRow;
25 ilm 18
import org.openconcerto.sql.model.SQLTableEvent;
19
import org.openconcerto.sql.model.SQLTableModifiedListener;
17 ilm 20
import org.openconcerto.sql.model.Where;
21
import org.openconcerto.sql.request.ComboSQLRequest;
22
import org.openconcerto.ui.component.text.TextComponent;
23
import org.openconcerto.utils.OrderedSet;
24
import org.openconcerto.utils.checks.MutableValueObject;
25
import org.openconcerto.utils.text.DocumentFilterList;
25 ilm 26
import org.openconcerto.utils.text.DocumentFilterList.FilterType;
17 ilm 27
import org.openconcerto.utils.text.LimitedSizeDocumentFilter;
28
 
29
import java.awt.Component;
30
import java.awt.GridLayout;
31
import java.awt.event.ComponentEvent;
32
import java.awt.event.ComponentListener;
33
import java.awt.event.FocusEvent;
34
import java.awt.event.FocusListener;
35
import java.awt.event.KeyEvent;
36
import java.awt.event.KeyListener;
37
import java.beans.PropertyChangeListener;
38
import java.beans.PropertyChangeSupport;
39
import java.util.Collection;
40
import java.util.HashMap;
41
import java.util.Iterator;
42
import java.util.List;
43
import java.util.Map;
44
import java.util.StringTokenizer;
45
import java.util.Vector;
46
 
47
import javax.swing.DefaultListModel;
48
import javax.swing.JComponent;
49
import javax.swing.JPanel;
50
import javax.swing.JTextArea;
51
import javax.swing.JTextField;
52
import javax.swing.SwingUtilities;
53
import javax.swing.SwingWorker;
54
import javax.swing.event.DocumentEvent;
55
import javax.swing.event.DocumentListener;
56
import javax.swing.text.AbstractDocument;
57
import javax.swing.text.DocumentFilter;
58
import javax.swing.text.JTextComponent;
59
 
60
public class ITextWithCompletion extends JPanel implements DocumentListener, TextComponent, MutableValueObject<String> {
61
 
62
    // FIXME asynchronous completion
63
 
64
    public static final int MODE_STARTWITH = 1;
65
    public static final int MODE_CONTAINS = 2;
66
 
67
    private int modeCompletion = MODE_CONTAINS;
68
    private static final long serialVersionUID = -6916931802603023440L;
69
 
70
    private JTextComponent text;
71
 
72
    private DefaultListModel model = new DefaultListModel();
73
 
74
    // lists de IComboSelectionItem
75
    private IComboSelectionItemCache mainCache = new IComboSelectionItemCache();
76
 
77
    private boolean completionEnabled = true;
78
 
79
    private int selectedId = -1;
80
 
81
    private boolean selectAuto = true;
82
 
83
    ComboSQLRequest comboRequest;
84
 
85
    protected ITextWithCompletionPopUp popup;
86
 
87
    OrderedSet<SelectionListener> listeners = new OrderedSet<SelectionListener>();
88
    Component popupInvoker;
89
 
90
    private boolean isLoading = false;
91
    private int idToSelect = -1;
92
 
93
    private String fillWith = null;
94
    private final PropertyChangeSupport supp;
95
 
96
    public ITextWithCompletion(ComboSQLRequest r, boolean multiline) {
97
 
98
        this.comboRequest = r;
99
        this.supp = new PropertyChangeSupport(this);
100
        this.popup = new ITextWithCompletionPopUp(this.model, this);
101
        final JTextField textField = new JTextField();
102
        if (!multiline) {
103
            this.text = textField;
104
            this.setLayout(new GridLayout(1, 1));
105
            this.add(this.text);
106
            setTextEditor(this.text);
107
            setPopupInvoker(this);
108
        } else {
109
            this.text = new JTextArea();
110
            this.text.setBorder(textField.getBorder());
111
            this.text.setFont(textField.getFont());
112
            this.setLayout(new GridLayout(1, 1));
113
            this.add(this.text);
114
            setTextEditor(this.text);
115
            setPopupInvoker(this);
116
        }
117
 
118
        this.isLoading = true;
119
        loadCacheAsynchronous();
25 ilm 120
        // FIXME never removed
121
        this.comboRequest.addTableListener(new SQLTableModifiedListener() {
122
            @Override
123
            public void tableModified(SQLTableEvent evt) {
17 ilm 124
                loadCacheAsynchronous();
125
            }
126
        });
127
    }
128
 
129
    public void setPopupListEnabled(boolean b) {
130
        this.popup.setListEnabled(b);
131
    }
132
 
133
    public void setTextEditor(final JTextComponent atext) {
134
        if (atext == null) {
135
            throw new IllegalArgumentException("null textEditor");
136
        }
137
        this.text = atext;
138
        atext.getDocument().addDocumentListener(this);
139
        atext.addKeyListener(new KeyListener() {
140
 
141
            private boolean consume;
142
 
143
            public void keyPressed(KeyEvent e) {
19 ilm 144
                if (e.getKeyCode() == KeyEvent.VK_TAB) {
145
                    // Complete si exactement la valeur souhaitée
146
                    updateAutoCompletion(true);
147
                    e.consume();
148
                } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
17 ilm 149
                    if (ITextWithCompletion.this.popup.isShowing()) {
150
                        ITextWithCompletion.this.popup.selectNext();
151
                        e.consume();
152
                    } else {
153
                        if (getSelectedId() <= 1) {
154
                            // updateAutoCompletion();
155
                            showPopup();
156
                        }
157
                    }
158
 
159
                } else if (e.getKeyCode() == KeyEvent.VK_UP) {
160
                    if (ITextWithCompletion.this.popup.isShowing()) {
161
                        ITextWithCompletion.this.popup.selectPrevious();
162
                        e.consume();
163
                    }
164
 
165
                } else if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_TAB) {
166
                    if (ITextWithCompletion.this.popup.isShowing()) {
167
                        ITextWithCompletion.this.popup.validateSelection();
168
                        e.consume();
169
                    }
170
                } else if (e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
171
                    if (ITextWithCompletion.this.popup.isShowing()) {
172
                        ITextWithCompletion.this.popup.selectNextPage();
173
                        e.consume();
174
                    }
175
                } else if (e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
176
                    if (ITextWithCompletion.this.popup.isShowing()) {
177
                        ITextWithCompletion.this.popup.selectPreviousPage();
178
                        e.consume();
179
                    }
19 ilm 180
                } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
181
                    if (ITextWithCompletion.this.popup.isShowing()) {
182
                        hidePopup();
17 ilm 183
                    }
19 ilm 184
 
17 ilm 185
                }
186
 
187
                // else {
188
                // if (e.getKeyCode() != KeyEvent.VK_RIGHT && e.getKeyCode() !=
189
                // KeyEvent.VK_LEFT) {
190
                // fireSelectionId(-1);
191
                // }
192
                // }
193
                // Evite les bips
194
                if (ITextWithCompletion.this.text.getDocument().getLength() == 0 && (e.getKeyCode() == KeyEvent.VK_DELETE || e.getKeyCode() == KeyEvent.VK_BACK_SPACE)) {
195
                    System.err.println("consume");
196
                    this.consume = true;
197
                    e.consume();
198
                }
199
 
200
            }
201
 
202
            public void keyReleased(KeyEvent e) {
203
            }
204
 
205
            public void keyTyped(KeyEvent e) {
206
                // Evite les bips
207
                if (this.consume) {
208
                    e.consume();
209
                    this.consume = false;
210
                }
211
            }
212
        });
213
        this.addComponentListener(new ComponentListener() {
214
            public void componentHidden(ComponentEvent e) {
215
            }
216
 
217
            public void componentMoved(ComponentEvent e) {
218
            }
219
 
220
            public void componentResized(ComponentEvent e) {
221
                // ajuste la taille min de la popup
222
                ITextWithCompletion.this.popup.setMinWith(atext.getBounds().width);
223
            }
224
 
225
            public void componentShown(ComponentEvent e) {
226
            }
227
        });
228
        atext.addFocusListener(new FocusListener() {
229
            @Override
230
            public void focusGained(FocusEvent e) {
231
 
232
            }
233
 
234
            @Override
235
            public void focusLost(FocusEvent e) {
236
                hidePopup();
237
            }
238
        });
239
    }
240
 
241
    /**
242
     * Load or reload the cache for completion
243
     */
244
    public void loadCache() {
245
        synchronized (this) {
246
            this.isLoading = true;
247
        }
248
        final List<IComboSelectionItem> comboItems = this.comboRequest.getComboItems();
249
        synchronized (this) {
250
            this.mainCache.clear();
251
            this.mainCache.addAll(comboItems);
252
            this.isLoading = false;
253
        }
254
    }
255
 
256
    void loadCacheAsynchronous() {
257
 
258
        synchronized (this) {
259
            this.isLoading = true;
260
        }
261
        final SwingWorker worker = new SwingWorker<Object, Object>() {
262
 
263
            // Runs on the event-dispatching thread.
264
            @Override
265
            public void done() {
266
                ITextWithCompletion.this.popup.getAccessibleContext().setAccessibleParent(ITextWithCompletion.this.text);
267
                // do not call updateAutoCompletion() otherwise the popup will be shown
268
                // although the user has not typed anything
269
                if (ITextWithCompletion.this.idToSelect != -1) {
270
                    selectId(ITextWithCompletion.this.idToSelect);
271
                    ITextWithCompletion.this.idToSelect = -1;
272
                }
273
            }
274
 
275
            @Override
276
            protected Object doInBackground() throws Exception {
277
                loadCache();
19 ilm 278
 
17 ilm 279
                return null;
280
            }
281
        };
19 ilm 282
 
17 ilm 283
        worker.execute();
284
    }
285
 
286
    /**
287
     * Retourne une liste de IComboSelectionItem, qui sont les selections possibles pour le text
288
     * passé
289
     */
290
    List<IComboSelectionItem> getPossibleValues(String aText) {
291
        List<IComboSelectionItem> result = new Vector<IComboSelectionItem>();
19 ilm 292
        if (aText.isEmpty()) {
293
            return result;
294
        }
17 ilm 295
        Map<String, IComboSelectionItem> map = new HashMap<String, IComboSelectionItem>();
296
 
297
        aText = aText.trim().toLowerCase();
298
        List<String> values = cut(aText);
299
        int stop = values.size();
300
 
301
        if (aText.length() > 0) {
302
            // car index(chaine vide) existe toujours...
303
            Collection<IComboSelectionItem> col = this.mainCache.getItems();
304
            for (IComboSelectionItem item : col) {
305
 
306
                boolean ok = false;
307
                final String lowerCase = item.getLabel().toLowerCase();
308
 
309
                for (int j = 0; j < stop; j++) {
310
 
311
                    if (this.modeCompletion == MODE_CONTAINS) {
312
                        if (lowerCase.indexOf(values.get(j)) >= 0) {
313
                            // ajout a la combo");
314
                            ok = true;
315
                        } else {
316
                            ok = false;
317
                            break;
318
                        }
319
                    } else {
320
                        if (lowerCase.startsWith(values.get(j))) {
321
                            // ajout a la combo");
322
                            ok = true;
323
                        } else {
324
                            ok = false;
325
                            break;
326
                        }
327
                    }
328
                }
329
 
330
                // FIXME: mettre dans les prefs removeDuplicate
331
                boolean removeDuplicate = true;
332
 
333
                if (ok) {
334
 
335
                    if (removeDuplicate) {
336
                        if (map.get(lowerCase) == null) {
337
                            map.put(lowerCase, item);
338
                            result.add(item);
339
                        }
340
                    } else {
341
                        result.add(item);
342
                    }
343
                }
344
            }
345
        }
346
 
347
        return result;
348
    }
349
 
350
    private List<String> cut(String value) {
19 ilm 351
        final Vector<String> v = new Vector<String>();
352
        final StringTokenizer tokenizer = new StringTokenizer(value);
17 ilm 353
        while (tokenizer.hasMoreElements()) {
19 ilm 354
            final String element = (String) tokenizer.nextElement();
17 ilm 355
            v.add(element.toLowerCase());
356
        }
357
        return v;
358
    }
359
 
19 ilm 360
    private void updateAutoCompletion(boolean autoselectIfMatch) {
17 ilm 361
        if (!this.isCompletionEnabled() || this.isLoading) {
362
            return;
363
        }
364
        String t = this.text.getText().trim();
365
 
366
        List<IComboSelectionItem> l = getPossibleValues(t); // Liste de IComboSelection
367
 
368
        // On cache la popup si le nombre de ligne change afin que sa taille soit correcte
369
        if (l.size() != this.model.size() && l.size() <= ITextWithCompletionPopUp.MAXROW) {
370
            hidePopup();
371
        }
372
        // on vide le model
373
        this.model.removeAllElements();
374
        for (Iterator<IComboSelectionItem> iter = l.iterator(); iter.hasNext();) {
375
            IComboSelectionItem element = iter.next();
376
            this.model.addElement(element);
377
        }
378
        if (l.size() > 0) {
379
            showPopup();
380
        } else {
381
            hidePopup();
382
        }
383
        // Le texte dans la case n'est pas celui d'un id
19 ilm 384
 
17 ilm 385
        int newId = this.selectedId;
386
        boolean found = false;
387
        for (Iterator<IComboSelectionItem> iter = l.iterator(); iter.hasNext();) {
388
            IComboSelectionItem element = iter.next();
19 ilm 389
            if (element.getLabel().equalsIgnoreCase(t) && autoselectIfMatch) {
17 ilm 390
                newId = element.getId();
391
                hidePopup();
392
                found = true;
393
                break;
394
            }
395
        }
396
        if (this.selectAuto && found && newId != this.selectedId) {
397
            this.selectedId = newId;
398
            SwingUtilities.invokeLater(new Runnable() {
399
                public void run() {
400
                    ITextWithCompletion.this.fireSelectionId(ITextWithCompletion.this.getSelectedId());
401
                }
19 ilm 402
            });
17 ilm 403
        }
404
        if (!found) {
405
            this.selectedId = -1;
406
            fireSelectionId(-1);
407
        }
408
    }
409
 
410
    public synchronized void hidePopup() {
411
        this.popup.setVisible(false);
412
    }
413
 
414
    private synchronized void showPopup() {
415
        if (this.model.size() > 0) {
416
            if (this.popupInvoker.isShowing())
417
                this.popup.show(this.popupInvoker, 0, this.text.getBounds().height);
418
        }
419
    }
420
 
421
    public void changedUpdate(DocumentEvent e) {
19 ilm 422
        updateAutoCompletion(false);
17 ilm 423
        this.supp.firePropertyChange("value", null, this.getText());
424
    }
425
 
426
    public void insertUpdate(DocumentEvent e) {
19 ilm 427
        updateAutoCompletion(false);
17 ilm 428
        this.supp.firePropertyChange("value", null, this.getText());
429
    }
430
 
431
    public void removeUpdate(DocumentEvent e) {
19 ilm 432
        updateAutoCompletion(false);
17 ilm 433
        this.supp.firePropertyChange("value", null, this.getText());
434
    }
435
 
436
    public int getSelectedId() {
437
        return this.selectedId;
438
    }
439
 
440
    public void setSelectedId(int selectedId) {
441
        this.selectedId = selectedId;
442
    }
443
 
444
    private void clearText() {
445
        setText("");
446
 
447
    }
448
 
449
    public void setEditable(boolean b) {
450
        this.text.setEditable(b);
451
 
452
    }
453
 
454
    public synchronized void selectId(int id) {
455
 
456
        if (this.isLoading) {
457
            this.idToSelect = id;
458
 
459
        } else {
460
            if (this.selectedId != id) {
461
                this.setSelectedId(id);
462
                this.selectItem(this.mainCache.getFromId(id));
463
                this.fireSelectionId(id);
464
            }
465
        }
466
    }
467
 
468
    public void setFillWithField(String s) {
469
        this.fillWith = s;
470
    }
471
 
472
    public SQLField getFillWithField() {
473
        return this.comboRequest.getPrimaryTable().getField(fillWith);
474
    }
475
 
476
    public void selectItem(IComboSelectionItem item) {
477
        if (!SwingUtilities.isEventDispatchThread()) {
478
            throw new IllegalStateException("Not in Swing!");
479
        }
480
        if (item != null) {
481
            if (this.fillWith != null) {
482
                SQLRow row = this.comboRequest.getPrimaryTable().getRow(item.getId());
483
                this.setText(row.getObject(this.fillWith).toString());
484
            } else {
485
                this.setText(item.getLabel());
486
            }
487
        } else {
488
            this.clearText();
489
        }
490
        hidePopup();
491
    }
492
 
493
    public void setText(final String label) {
494
        if (!SwingUtilities.isEventDispatchThread()) {
495
            throw new IllegalStateException("Not in Swing!");
496
        }
497
        setCompletionEnabled(false);
498
 
499
        this.text.setText(label);
500
        if (label != null) {
501
            this.text.setCaretPosition(label.length());
502
        }
503
        this.text.repaint();
504
        setCompletionEnabled(true);
505
    }
506
 
507
    // Gestion des listeners de selection d'id
508
    public void addSelectionListener(SelectionListener l) {
509
        this.listeners.add(l);
510
    }
511
 
512
    public void removeSelectionListener(SelectionListener l) {
513
        this.listeners.remove(l);
514
    }
515
 
516
    private boolean isDispatching = false;
517
 
518
    private void fireSelectionId(int id) {
519
        if (!this.isDispatching) {
520
            this.isDispatching = true;
521
            for (Iterator<SelectionListener> iter = this.listeners.iterator(); iter.hasNext();) {
522
                SelectionListener element = iter.next();
523
                element.idSelected(id, this);
524
            }
525
            this.isDispatching = false;
526
        }
527
    }
528
 
529
    /**
530
     * @return Returns the completionEnabled.
531
     */
532
    boolean isCompletionEnabled() {
533
        return this.completionEnabled;
534
    }
535
 
536
    /**
537
     * @param completionEnabled The completionEnabled to set.
538
     */
539
    void setCompletionEnabled(boolean completionEnabled) {
540
        this.completionEnabled = completionEnabled;
541
    }
542
 
543
    public Object getText() {
544
 
545
        return this.text.getText();
546
    }
547
 
548
    /**
549
     * @param popupInvoker The popupInvoker to set.
550
     */
551
    public void setPopupInvoker(Component popupInvoker) {
552
        this.popupInvoker = popupInvoker;
553
    }
554
 
555
    /**
556
     * Mode de completion startwith ou contains
557
     *
558
     * @param mode
559
     *
560
     */
561
    public void setModeCompletion(int mode) {
562
        this.modeCompletion = mode;
563
    }
564
 
565
    public JTextComponent getTextComp() {
566
        return this.text;
567
    }
568
 
569
    public JComponent getComp() {
570
        return this;
571
    }
572
 
573
    public void setSelectionAutoEnabled(boolean b) {
574
        this.selectAuto = b;
575
    }
576
 
577
    public void setWhere(Where w) {
578
        this.comboRequest.setWhere(w);
579
        loadCacheAsynchronous();
580
    }
581
 
582
    public void setLimitedSize(int nbChar) {
583
        // rm previous ones
584
        final DocumentFilterList dfl = DocumentFilterList.get((AbstractDocument) this.text.getDocument());
585
        final Iterator<DocumentFilter> iter = dfl.getFilters().iterator();
586
        while (iter.hasNext()) {
587
            final DocumentFilter df = iter.next();
588
            if (df instanceof LimitedSizeDocumentFilter)
589
                iter.remove();
590
        }
591
        // add the new one
592
        DocumentFilterList.add((AbstractDocument) this.text.getDocument(), new LimitedSizeDocumentFilter(nbChar), FilterType.SIMPLE_FILTER);
593
    }
594
 
595
    @Override
596
    public void resetValue() {
597
        this.setText("");
598
    }
599
 
600
    @Override
601
    public void setValue(String val) {
602
        this.setText(val);
603
    }
604
 
605
    @Override
606
    public void addValueListener(PropertyChangeListener l) {
607
        this.supp.addPropertyChangeListener(l);
608
    }
609
 
610
    @Override
611
    public String getValue() {
612
        return (String) this.getText();
613
    }
614
 
615
    @Override
616
    public void rmValueListener(PropertyChangeListener l) {
617
        this.supp.removePropertyChangeListener(l);
618
    }
619
 
620
}