OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 83 | 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.search;
15
 
73 ilm 16
import org.openconcerto.sql.TM;
17 ilm 17
import org.openconcerto.sql.element.BaseSQLComponent;
18
import org.openconcerto.sql.view.search.TextSearchSpec.Mode;
61 ilm 19
import org.openconcerto.utils.CompareUtils;
83 ilm 20
import org.openconcerto.utils.ListMap;
17 ilm 21
import org.openconcerto.utils.model.ListComboBoxModel;
22
import org.openconcerto.utils.text.SimpleDocumentListener;
23
 
24
import java.awt.Component;
25
import java.awt.Dimension;
26
import java.awt.GridBagConstraints;
27
import java.awt.GridBagLayout;
28
import java.awt.Insets;
29
import java.awt.event.ActionEvent;
30
import java.awt.event.ActionListener;
73 ilm 31
import java.awt.event.HierarchyEvent;
32
import java.awt.event.HierarchyListener;
17 ilm 33
import java.awt.event.ItemEvent;
34
import java.awt.event.ItemListener;
35
import java.util.ArrayList;
36
import java.util.Arrays;
37
import java.util.Collection;
38
import java.util.List;
39
import java.util.Map.Entry;
40
import java.util.SortedMap;
41
import java.util.TreeMap;
42
 
80 ilm 43
import javax.swing.BorderFactory;
17 ilm 44
import javax.swing.ImageIcon;
45
import javax.swing.JButton;
46
import javax.swing.JCheckBox;
47
import javax.swing.JComboBox;
48
import javax.swing.JList;
49
import javax.swing.JPanel;
50
import javax.swing.JTextField;
51
import javax.swing.ListCellRenderer;
52
import javax.swing.event.DocumentEvent;
61 ilm 53
import javax.swing.event.TableModelEvent;
54
import javax.swing.event.TableModelListener;
182 ilm 55
import javax.swing.table.TableModel;
17 ilm 56
import javax.swing.text.BadLocationException;
57
 
73 ilm 58
import net.jcip.annotations.Immutable;
59
 
17 ilm 60
public class SearchItemComponent extends JPanel {
73 ilm 61
    private static final Column TOUT = new Column(TM.tr("all"), null, -1);
17 ilm 62
    private static final Mode[] MODES = { Mode.CONTAINS, Mode.CONTAINS_STRICT, Mode.LESS_THAN, Mode.EQUALS, Mode.EQUALS_STRICT, Mode.GREATER_THAN };
63
 
73 ilm 64
    @Immutable
61 ilm 65
    protected static final class Column {
66
        private final String label, id;
67
        private final int index;
68
 
69
        protected Column(String label, String id, int index) {
70
            super();
71
            this.label = label;
72
            this.id = id;
73
            this.index = index;
74
        }
75
 
76
        // the current label of the column
77
        // (not always the same for a column, it depends on names of the other columns)
78
        public final String getLabel() {
79
            return this.label;
80
        }
81
 
82
        // an identifier for the column, should never change
83
        public final String getID() {
84
            return this.id;
85
        }
86
 
87
        // the index of the column in the table model
88
        public final int getIndex() {
89
            return this.index;
90
        }
91
    }
92
 
182 ilm 93
    private final JTextField textFieldRecherche = new JTextField(10);
94
    private final JComboBox<Column> comboColonnePourRecherche;
95
    private final JComboBox<String> searchMode;
96
    private final JCheckBox invertSearch = new JCheckBox(TM.tr("toReverse"));
97
    private final JButton buttonAdd = new JButton("+");
98
    private final JButton buttonRemove = new JButton();
17 ilm 99
    final SearchListComponent list;
182 ilm 100
    // final to ease removing listener, SearchListComponent.reset() removes every item
101
    // when the TableModel changes.
102
    private final TableModel tableModel;
17 ilm 103
    private String text = "";
104
 
105
    public SearchItemComponent(final SearchListComponent list) {
106
        super();
107
        this.list = list;
182 ilm 108
        this.tableModel = this.list.getTableModel();
17 ilm 109
        this.setOpaque(false);
110
        // Initialisation de l'interface graphique
182 ilm 111
        this.searchMode = new JComboBox<>(
112
                new String[] { TM.tr("contains"), TM.tr("contains.exactly"), TM.tr("isLessThan"), TM.tr("isEqualTo"), TM.tr("isExactlyEqualTo"), TM.tr("isGreaterThan"), TM.tr("isEmpty") });
113
        final ListComboBoxModel<Column> comboModel = new ListComboBoxModel<>(Arrays.asList(TOUT));
61 ilm 114
        comboModel.setSelectOnAdd(false);
73 ilm 115
        // allow getColIndex() and thus getSearchItem() to work from now on
116
        assert comboModel.getSelectedItem() != null;
182 ilm 117
        this.comboColonnePourRecherche = new JComboBox<>(comboModel);
17 ilm 118
        uiInit();
119
    }
120
 
182 ilm 121
    final TableModel getTableModel() {
122
        return this.tableModel;
123
    }
124
 
17 ilm 125
    private void uiInit() {
126
        this.setLayout(new GridBagLayout());
127
        final GridBagConstraints c = new GridBagConstraints();
128
        c.gridx = 0;
129
        c.gridy = 0;
130
        c.insets = new Insets(0, 2, 0, 2);
131
        c.fill = GridBagConstraints.HORIZONTAL;
132
 
133
        // designation
134
        // don't just use DefaultListCellRenderer, it fails on some l&f
182 ilm 135
        @SuppressWarnings("unchecked")
136
        final ListCellRenderer<Object> old = (ListCellRenderer<Object>) this.comboColonnePourRecherche.getRenderer();
137
        this.comboColonnePourRecherche.setRenderer(new ListCellRenderer<Column>() {
17 ilm 138
            @Override
182 ilm 139
            public Component getListCellRendererComponent(JList<? extends Column> list, Column value, int index, boolean isSelected, boolean cellHasFocus) {
140
                return old.getListCellRendererComponent(list, value.getLabel(), index, isSelected, cellHasFocus);
17 ilm 141
            }
142
        });
143
        // hand tuned for a IListPanel width of 1024px
144
        this.comboColonnePourRecherche.setMinimumSize(new Dimension(150, 20));
41 ilm 145
        this.comboColonnePourRecherche.setOpaque(false);
17 ilm 146
        add(this.comboColonnePourRecherche, c);
147
        c.gridx++;
148
        // contient
149
        this.searchMode.setMinimumSize(new Dimension(40, 20));
41 ilm 150
        this.searchMode.setOpaque(false);
17 ilm 151
        add(this.searchMode, c);
152
        c.gridx++;
153
        // Texte de recherche
154
        c.weightx = 1;
155
        // about 10 characters
156
        this.textFieldRecherche.setMinimumSize(new Dimension(50, 20));
157
        add(this.textFieldRecherche, c);
158
        c.weightx = 0;
159
        c.gridx++;
160
        // inversion de la recherche
161
        this.invertSearch.setOpaque(false);
162
        if (!Boolean.getBoolean("org.openconcerto.ui.removeSwapSearchCheckBox")) {
163
            add(this.invertSearch, c);
164
        }
165
        // ajout d'un element de recherche
166
        c.gridx++;
41 ilm 167
        this.buttonAdd.setOpaque(false);
17 ilm 168
        add(this.buttonAdd, c);
169
        // supprime un element de recherche
170
        c.gridx++;
171
        this.buttonRemove.setIcon(new ImageIcon(BaseSQLComponent.class.getResource("delete.png")));
80 ilm 172
        this.buttonRemove.setBorder(BorderFactory.createEmptyBorder());
17 ilm 173
        this.buttonRemove.setOpaque(false);
174
        this.buttonRemove.setBorderPainted(false);
175
        this.buttonRemove.setFocusPainted(false);
176
        this.buttonRemove.setContentAreaFilled(false);
177
        add(this.buttonRemove, c);
178
 
179
        initCombo();
180
        initSearchText();
181
        initInvertSearch();
182
 
183
        this.buttonAdd.addActionListener(new ActionListener() {
182 ilm 184
            @Override
17 ilm 185
            public void actionPerformed(ActionEvent e) {
186
                SearchItemComponent.this.list.addNewSearchItem();
187
            }
188
        });
189
        this.buttonRemove.addActionListener(new ActionListener() {
182 ilm 190
            @Override
17 ilm 191
            public void actionPerformed(ActionEvent e) {
192
                SearchItemComponent.this.list.removeSearchItem(SearchItemComponent.this);
193
            }
194
        });
195
    }
196
 
197
    private void initInvertSearch() {
198
        this.invertSearch.addActionListener(new ActionListener() {
182 ilm 199
            @Override
17 ilm 200
            public void actionPerformed(ActionEvent e) {
201
                updateSearchList();
202
            }
203
        });
204
    }
205
 
206
    private void initSearchText() {
207
        this.textFieldRecherche.getDocument().addDocumentListener(new SimpleDocumentListener() {
182 ilm 208
            @Override
17 ilm 209
            public void update(DocumentEvent e) {
210
                try {
211
                    // One ne peut pas appeler chercher() car le texte n'est pas encore a jour
212
                    SearchItemComponent.this.text = e.getDocument().getText(0, e.getDocument().getLength()).trim();
213
                    updateSearchList();
182 ilm 214
                } catch (final BadLocationException exn) {
17 ilm 215
                    // impossible
216
                    exn.printStackTrace();
217
                }
218
            }
219
        });
220
    }
221
 
222
    private void initCombo() {
61 ilm 223
        final ItemListener listener = new ItemListener() {
182 ilm 224
            @Override
61 ilm 225
            public void itemStateChanged(ItemEvent e) {
226
                updateSearchList();
227
            }
228
        };
229
        this.searchMode.addItemListener(listener);
230
 
73 ilm 231
        final TableModelListener tableModelL = new TableModelListener() {
61 ilm 232
            @Override
233
            public void tableChanged(TableModelEvent e) {
234
                if (e.getColumn() == TableModelEvent.ALL_COLUMNS && e.getFirstRow() == TableModelEvent.HEADER_ROW) {
235
                    columnsChanged(listener);
236
                }
237
            }
73 ilm 238
        };
239
        // allow the TableModel to die
240
        this.addHierarchyListener(new HierarchyListener() {
182 ilm 241
            @Override
73 ilm 242
            public void hierarchyChanged(HierarchyEvent e) {
243
                if ((e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0) {
244
                    if (e.getChanged().isDisplayable()) {
245
                        columnsChanged(listener);
182 ilm 246
                        SearchItemComponent.this.getTableModel().addTableModelListener(tableModelL);
73 ilm 247
                    } else {
182 ilm 248
                        SearchItemComponent.this.getTableModel().removeTableModelListener(tableModelL);
73 ilm 249
                    }
250
                }
251
            }
61 ilm 252
        });
73 ilm 253
        // that way the TableModelListener will get added automatically
254
        assert !this.isDisplayable();
61 ilm 255
    }
256
 
257
    private void selectAllColumnsItem() {
258
        this.comboColonnePourRecherche.setSelectedIndex(0);
259
    }
260
 
261
    private void fillColumnCombo(ItemListener listener) {
17 ilm 262
        // sort column names alphabetically
182 ilm 263
        final int columnCount = this.getTableModel().getColumnCount();
17 ilm 264
        final String[][] names = new String[columnCount][];
265
        final int[] indexes = new int[columnCount];
266
        for (int i = 0; i < columnCount; i++) {
267
            names[i] = this.list.getColumnNames(i);
268
            indexes[i] = 0;
269
        }
270
        // use column index as columns names are not unique
271
        final SortedMap<String, Integer> map = solve(names, indexes);
182 ilm 272
        final List<Column> cols = new ArrayList<>(columnCount);
73 ilm 273
        cols.add(TOUT);
61 ilm 274
        for (final Entry<String, Integer> e : map.entrySet()) {
275
            final int colIndex = e.getValue().intValue();
276
            final String[] colNames = names[colIndex];
277
            cols.add(new Column(e.getKey(), colNames[colNames.length - 1], colIndex));
278
        }
17 ilm 279
 
61 ilm 280
        // don't fire when filling, we will fire when selecting
281
        this.comboColonnePourRecherche.removeItemListener(listener);
182 ilm 282
        final ListComboBoxModel<Column> comboModel = (ListComboBoxModel<Column>) this.comboColonnePourRecherche.getModel();
61 ilm 283
        assert !comboModel.isSelectOnAdd() : "Otherwise our following select might not fire";
284
        comboModel.removeAllElements();
285
        comboModel.addAll(cols);
17 ilm 286
        this.comboColonnePourRecherche.addItemListener(listener);
287
    }
288
 
61 ilm 289
    private void columnsChanged(final ItemListener listener) {
290
        final String currentID = ((Column) this.comboColonnePourRecherche.getSelectedItem()).getID();
291
        fillColumnCombo(listener);
182 ilm 292
        final ListComboBoxModel<Column> comboModel = (ListComboBoxModel<Column>) this.comboColonnePourRecherche.getModel();
61 ilm 293
        // no selection since the model was just emptied
294
        assert this.comboColonnePourRecherche.getSelectedIndex() == -1 && this.comboColonnePourRecherche.getSelectedItem() == null;
295
        // try to reselect the same column if it's still there
296
        for (final Object o : comboModel.getList()) {
297
            final Column col = (Column) o;
298
            if (CompareUtils.equals(col.getID(), currentID))
299
                this.comboColonnePourRecherche.setSelectedItem(o);
300
        }
301
        if (comboModel.getSelectedItem() == null)
302
            selectAllColumnsItem();
303
    }
304
 
17 ilm 305
    /**
306
     * Return a sorted map of column index by name.
307
     *
308
     * @param names all possible names for each column.
309
     * @param indexes index of the current name for each column.
310
     * @return a sorted map.
311
     */
312
    private SortedMap<String, Integer> solve(final String[][] names, final int[] indexes) {
313
        final int columnCount = names.length;
314
        // columns' index by name
182 ilm 315
        final ListMap<String, Integer> collisions = new ListMap<>(columnCount);
17 ilm 316
        for (int i = 0; i < columnCount; i++) {
317
            final int index = indexes[i];
318
            if (index >= names[i].length)
319
                throw new IllegalStateException("Ran out of names for " + i + " : " + Arrays.asList(names[i]));
320
            final String columnName = names[i][index];
83 ilm 321
            collisions.add(columnName, i);
17 ilm 322
        }
182 ilm 323
        final SortedMap<String, Integer> res = new TreeMap<>();
324
        for (final Entry<String, ? extends Collection<Integer>> e : collisions.entrySet()) {
17 ilm 325
            final Collection<Integer> indexesWithCollision = e.getValue();
326
            if (indexesWithCollision.size() > 1) {
327
                // increment only the minimum indexes to try to solve the conflict with the lowest
328
                // possible indexes
329
                int minIndex = Integer.MAX_VALUE;
330
                for (final Integer i : indexesWithCollision) {
331
                    if (indexes[i] < minIndex)
332
                        minIndex = indexes[i];
333
                }
334
                // now increment all indexes equal to minimum
335
                for (final Integer i : indexesWithCollision) {
336
                    if (indexes[i] == minIndex)
337
                        indexes[i]++;
338
                }
339
            } else {
340
                res.put(e.getKey(), indexesWithCollision.iterator().next());
341
            }
342
        }
343
        if (res.size() == columnCount)
344
            return res;
345
        else
346
            return solve(names, indexes);
347
    }
348
 
349
    void updateSearchList() {
350
        SearchItemComponent.this.list.updateSearch();
351
    }
352
 
353
    public SearchSpec getSearchItem() {
354
        final SearchSpec res;
355
        if (this.searchMode.getSelectedIndex() < MODES.length) {
356
            final TextSearchSpec textSpec = new TextSearchSpec(this.getText(), MODES[this.searchMode.getSelectedIndex()]);
357
            textSpec.setFormats(this.list.getFormats());
358
            res = textSpec;
359
        } else {
360
            res = new EmptySearchSpec();
361
        }
362
        return new ColumnSearchSpec(this.isExcluded(), res, this.getColIndex());
363
    }
364
 
365
    // *** state
366
 
367
    private final boolean isExcluded() {
368
        return this.invertSearch.isSelected();
369
    }
370
 
371
    private final int getColIndex() {
61 ilm 372
        return ((Column) this.comboColonnePourRecherche.getSelectedItem()).getIndex();
17 ilm 373
    }
374
 
375
    private final String getText() {
376
        return this.text;
377
    }
378
 
379
    public final void setText(String s) {
380
        this.textFieldRecherche.setText(s);
381
    }
382
 
383
    /**
384
     * Reinitialise le composant de recherche
385
     */
386
    public void resetState() {
387
        this.setText("");
61 ilm 388
        selectAllColumnsItem();
17 ilm 389
        this.searchMode.setSelectedIndex(0);
390
        this.invertSearch.setSelected(false);
391
    }
392
 
393
    public void setSearchFullMode(boolean b) {
394
        this.invertSearch.setVisible(b);
395
        this.buttonAdd.setVisible(b);
396
        this.buttonRemove.setVisible(b);
397
        this.comboColonnePourRecherche.setVisible(b);
398
        this.searchMode.setVisible(b);
399
    }
400
 
401
}