OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 67 | Rev 93 | 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;
63 ilm 24
import org.openconcerto.utils.SwingWorker2;
17 ilm 25
import org.openconcerto.utils.checks.MutableValueObject;
41 ilm 26
import org.openconcerto.utils.model.DefaultIMutableListModel;
17 ilm 27
import org.openconcerto.utils.text.DocumentFilterList;
25 ilm 28
import org.openconcerto.utils.text.DocumentFilterList.FilterType;
17 ilm 29
import org.openconcerto.utils.text.LimitedSizeDocumentFilter;
30
 
31
import java.awt.Component;
32
import java.awt.GridLayout;
33
import java.awt.event.ComponentEvent;
34
import java.awt.event.ComponentListener;
35
import java.awt.event.FocusEvent;
36
import java.awt.event.FocusListener;
37
import java.awt.event.KeyEvent;
38
import java.awt.event.KeyListener;
39
import java.beans.PropertyChangeListener;
40
import java.beans.PropertyChangeSupport;
41
import java.util.Collection;
42
import java.util.HashMap;
43
import java.util.Iterator;
44
import java.util.List;
45
import java.util.Map;
46
import java.util.StringTokenizer;
47
import java.util.Vector;
48
 
49
import javax.swing.JComponent;
50
import javax.swing.JPanel;
51
import javax.swing.JTextArea;
52
import javax.swing.JTextField;
53
import javax.swing.SwingUtilities;
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
 
83 ilm 60
public class ITextWithCompletion extends JPanel implements DocumentListener, TextComponent, MutableValueObject<String>, IComboSelectionItemListener {
17 ilm 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
 
41 ilm 72
    private DefaultIMutableListModel<IComboSelectionItem> model = new DefaultIMutableListModel<IComboSelectionItem>();
17 ilm 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
        }
63 ilm 261
        final SwingWorker2<Object, Object> worker = new SwingWorker2<Object, Object>() {
17 ilm 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
41 ilm 369
        if (l.size() != this.model.getSize() && l.size() <= ITextWithCompletionPopUp.MAXROW) {
17 ilm 370
            hidePopup();
371
        }
372
        // on vide le model
373
        this.model.removeAllElements();
41 ilm 374
        this.model.addAll(l);
375
        // for (Iterator<IComboSelectionItem> iter = l.iterator(); iter.hasNext();) {
376
        // IComboSelectionItem element = iter.next();
377
        // this.model.addElement(element);
378
        // }
17 ilm 379
        if (l.size() > 0) {
380
            showPopup();
381
        } else {
382
            hidePopup();
383
        }
384
        // Le texte dans la case n'est pas celui d'un id
19 ilm 385
 
17 ilm 386
        int newId = this.selectedId;
387
        boolean found = false;
388
        for (Iterator<IComboSelectionItem> iter = l.iterator(); iter.hasNext();) {
389
            IComboSelectionItem element = iter.next();
19 ilm 390
            if (element.getLabel().equalsIgnoreCase(t) && autoselectIfMatch) {
17 ilm 391
                newId = element.getId();
392
                hidePopup();
393
                found = true;
394
                break;
395
            }
396
        }
397
        if (this.selectAuto && found && newId != this.selectedId) {
398
            this.selectedId = newId;
399
            SwingUtilities.invokeLater(new Runnable() {
400
                public void run() {
401
                    ITextWithCompletion.this.fireSelectionId(ITextWithCompletion.this.getSelectedId());
402
                }
19 ilm 403
            });
17 ilm 404
        }
405
        if (!found) {
406
            this.selectedId = -1;
407
            fireSelectionId(-1);
408
        }
409
    }
410
 
411
    public synchronized void hidePopup() {
412
        this.popup.setVisible(false);
413
    }
414
 
415
    private synchronized void showPopup() {
41 ilm 416
        if (this.model.getSize() > 0) {
17 ilm 417
            if (this.popupInvoker.isShowing())
418
                this.popup.show(this.popupInvoker, 0, this.text.getBounds().height);
419
        }
420
    }
421
 
422
    public void changedUpdate(DocumentEvent e) {
19 ilm 423
        updateAutoCompletion(false);
17 ilm 424
        this.supp.firePropertyChange("value", null, this.getText());
425
    }
426
 
427
    public void insertUpdate(DocumentEvent e) {
19 ilm 428
        updateAutoCompletion(false);
17 ilm 429
        this.supp.firePropertyChange("value", null, this.getText());
430
    }
431
 
432
    public void removeUpdate(DocumentEvent e) {
19 ilm 433
        updateAutoCompletion(false);
17 ilm 434
        this.supp.firePropertyChange("value", null, this.getText());
435
    }
436
 
437
    public int getSelectedId() {
438
        return this.selectedId;
439
    }
440
 
441
    public void setSelectedId(int selectedId) {
442
        this.selectedId = selectedId;
443
    }
444
 
445
    private void clearText() {
446
        setText("");
447
 
448
    }
449
 
450
    public void setEditable(boolean b) {
451
        this.text.setEditable(b);
452
 
453
    }
454
 
83 ilm 455
    @Override
456
    public void itemSelected(IComboSelectionItem item) {
457
        selectId(item.getId());
458
    }
459
 
17 ilm 460
    public synchronized void selectId(int id) {
461
 
462
        if (this.isLoading) {
463
            this.idToSelect = id;
464
 
465
        } else {
466
            if (this.selectedId != id) {
467
                this.setSelectedId(id);
468
                this.selectItem(this.mainCache.getFromId(id));
469
                this.fireSelectionId(id);
470
            }
471
        }
472
    }
473
 
474
    public void setFillWithField(String s) {
475
        this.fillWith = s;
476
    }
477
 
478
    public SQLField getFillWithField() {
479
        return this.comboRequest.getPrimaryTable().getField(fillWith);
480
    }
481
 
482
    public void selectItem(IComboSelectionItem item) {
483
        if (!SwingUtilities.isEventDispatchThread()) {
484
            throw new IllegalStateException("Not in Swing!");
485
        }
486
        if (item != null) {
487
            if (this.fillWith != null) {
67 ilm 488
                // FIXME SQL request in Swing
17 ilm 489
                SQLRow row = this.comboRequest.getPrimaryTable().getRow(item.getId());
490
                this.setText(row.getObject(this.fillWith).toString());
491
            } else {
492
                this.setText(item.getLabel());
493
            }
494
        } else {
495
            this.clearText();
496
        }
497
        hidePopup();
498
    }
499
 
500
    public void setText(final String label) {
501
        if (!SwingUtilities.isEventDispatchThread()) {
502
            throw new IllegalStateException("Not in Swing!");
503
        }
504
        setCompletionEnabled(false);
505
 
506
        this.text.setText(label);
507
        if (label != null) {
508
            this.text.setCaretPosition(label.length());
509
        }
510
        this.text.repaint();
511
        setCompletionEnabled(true);
512
    }
513
 
514
    // Gestion des listeners de selection d'id
515
    public void addSelectionListener(SelectionListener l) {
516
        this.listeners.add(l);
517
    }
518
 
519
    public void removeSelectionListener(SelectionListener l) {
520
        this.listeners.remove(l);
521
    }
522
 
523
    private boolean isDispatching = false;
524
 
525
    private void fireSelectionId(int id) {
526
        if (!this.isDispatching) {
527
            this.isDispatching = true;
528
            for (Iterator<SelectionListener> iter = this.listeners.iterator(); iter.hasNext();) {
529
                SelectionListener element = iter.next();
530
                element.idSelected(id, this);
531
            }
532
            this.isDispatching = false;
533
        }
534
    }
535
 
536
    /**
537
     * @return Returns the completionEnabled.
538
     */
539
    boolean isCompletionEnabled() {
540
        return this.completionEnabled;
541
    }
542
 
543
    /**
544
     * @param completionEnabled The completionEnabled to set.
545
     */
546
    void setCompletionEnabled(boolean completionEnabled) {
547
        this.completionEnabled = completionEnabled;
548
    }
549
 
550
    public Object getText() {
551
 
552
        return this.text.getText();
553
    }
554
 
555
    /**
556
     * @param popupInvoker The popupInvoker to set.
557
     */
558
    public void setPopupInvoker(Component popupInvoker) {
559
        this.popupInvoker = popupInvoker;
560
    }
561
 
562
    /**
563
     * Mode de completion startwith ou contains
564
     *
565
     * @param mode
566
     *
567
     */
568
    public void setModeCompletion(int mode) {
569
        this.modeCompletion = mode;
570
    }
571
 
572
    public JTextComponent getTextComp() {
573
        return this.text;
574
    }
575
 
576
    public JComponent getComp() {
577
        return this;
578
    }
579
 
580
    public void setSelectionAutoEnabled(boolean b) {
581
        this.selectAuto = b;
582
    }
583
 
584
    public void setWhere(Where w) {
585
        this.comboRequest.setWhere(w);
586
        loadCacheAsynchronous();
587
    }
588
 
589
    public void setLimitedSize(int nbChar) {
590
        // rm previous ones
591
        final DocumentFilterList dfl = DocumentFilterList.get((AbstractDocument) this.text.getDocument());
592
        final Iterator<DocumentFilter> iter = dfl.getFilters().iterator();
593
        while (iter.hasNext()) {
594
            final DocumentFilter df = iter.next();
595
            if (df instanceof LimitedSizeDocumentFilter)
596
                iter.remove();
597
        }
598
        // add the new one
599
        DocumentFilterList.add((AbstractDocument) this.text.getDocument(), new LimitedSizeDocumentFilter(nbChar), FilterType.SIMPLE_FILTER);
600
    }
601
 
602
    @Override
603
    public void resetValue() {
604
        this.setText("");
605
    }
606
 
607
    @Override
608
    public void setValue(String val) {
609
        this.setText(val);
610
    }
611
 
612
    @Override
613
    public void addValueListener(PropertyChangeListener l) {
614
        this.supp.addPropertyChangeListener(l);
615
    }
616
 
617
    @Override
618
    public String getValue() {
619
        return (String) this.getText();
620
    }
621
 
622
    @Override
623
    public void rmValueListener(PropertyChangeListener l) {
624
        this.supp.removePropertyChangeListener(l);
625
    }
626
 
627
}