OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 144 | 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.ui.list.selection;
15
 
16
import static org.openconcerto.ui.list.selection.BaseListStateModel.INVALID_ID;
17
import static org.openconcerto.ui.list.selection.BaseListStateModel.INVALID_INDEX;
144 ilm 18
 
17 ilm 19
import org.openconcerto.ui.Log;
20
import org.openconcerto.utils.CollectionUtils;
21
import org.openconcerto.utils.cc.ITransformer;
22
 
23
import java.beans.PropertyChangeEvent;
24
import java.beans.PropertyChangeListener;
25
import java.beans.PropertyChangeSupport;
26
import java.util.ArrayList;
27
import java.util.Arrays;
28
import java.util.Collection;
29
import java.util.Collections;
30
import java.util.HashMap;
31
import java.util.HashSet;
32
import java.util.List;
33
import java.util.Map;
34
import java.util.Set;
35
import java.util.SortedMap;
36
import java.util.SortedSet;
37
import java.util.TreeMap;
38
import java.util.TreeSet;
39
 
40
import javax.swing.ListSelectionModel;
41
import javax.swing.event.ListSelectionEvent;
42
import javax.swing.event.ListSelectionListener;
43
 
44
/**
45
 * A class that maintain the selection of a list (via a ListSelectionModel) not with an index but
46
 * with a unique ID. That means that after the items of the list change (eg a search is performed),
47
 * the selection is set to the previous ID not the previous index (note that the same ID might not
48
 * mean the same object).
49
 *
50
 * @author Sylvain
51
 */
52
public final class ListSelectionState implements ListSelection {
53
 
54
    public static ListSelectionState manage(final ListSelectionModel sel, final BaseListStateModel model) {
55
        return new ListSelectionState(model, sel).start();
56
    }
57
 
58
    // * models
59
    private final BaseListStateModel model;
60
    private final ListSelectionModel selModel;
61
 
62
    // * selection
63
    // {index => ID}, les index et ID des lignes sélectionnées
64
    private final SortedMap<Integer, Integer> selection;
65
    // les ID volontairement sélectionnés (eg != de selection si recherche)
66
    private final Set<Integer> userSelectedIDs;
67
 
68
    // * listeners
69
    private final PropertyChangeListener updateListener;
70
    private final ListSelectionListener selectionListener;
71
 
72
    private final PropertyChangeSupport supp;
73
 
74
    private boolean updating;
75
    private boolean strict;
76
 
77
    /**
78
     * Create a new instance.
79
     *
80
     * @param model to listen to item changes.
81
     * @param sel to listen to selection changes.
82
     */
83
    private ListSelectionState(final BaseListStateModel model, final ListSelectionModel sel) {
84
        this.model = model;
85
        this.selModel = sel;
86
 
87
        this.supp = new PropertyChangeSupport(this);
88
        this.updating = false;
89
        this.strict = false;
90
        this.selection = new TreeMap<Integer, Integer>();
91
        this.userSelectedIDs = new HashSet<Integer>();
92
 
93
        this.updateListener = new PropertyChangeListener() {
94
            // pour resélectionner après une maj
95
            public void propertyChange(PropertyChangeEvent evt) {
96
                final String propName = evt.getPropertyName();
97
                if (propName.equals("updating")) {
98
                    ListSelectionState.this.setUpdating((Boolean) evt.getNewValue());
99
                }
100
            }
101
        };
102
        this.selectionListener = new ListSelectionListener() {
103
            public void valueChanged(ListSelectionEvent e) {
104
                // ne pas filtrer les ValueIsAdjusting pour etre dynamique
105
                rowSelected(e);
106
            }
107
        };
108
 
109
        start();
110
    }
111
 
112
    private ListSelectionState start() {
113
        this.model.addListener(this.updateListener);
114
        this.getSelModel().addListSelectionListener(this.selectionListener);
115
        return this;
116
    }
117
 
118
    void stop() {
119
        this.model.rmListener(this.updateListener);
120
        this.getSelModel().removeListSelectionListener(this.selectionListener);
121
    }
122
 
123
    public final BaseListStateModel getModel() {
124
        return this.model;
125
    }
126
 
127
    protected final ListSelectionModel getSelModel() {
128
        return this.selModel;
129
    }
130
 
131
    /*
132
     * Nous prévient qu'une ligne a été sélectionnée.
133
     */
134
    private void rowSelected(ListSelectionEvent e) {
135
        // compute the new selection
136
        final Map<Integer, Integer> newIDs = new HashMap<Integer, Integer>(this.getSelection());
137
        for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
138
            if (!this.getSelModel().isSelectedIndex(i))
139
                newIDs.remove(i);
140
            else {
141
                final int id = this.idFromIndex(i);
142
                if (id == INVALID_ID)
143
                    throw new IllegalStateException("selected index " + i + " has no id");
144
                newIDs.put(i, id);
145
            }
146
        }
147
 
148
        // filtrer sur les réels changements car le ListSelectionListener
149
        // nous envoie absolument tous les changements de sélection
150
        if (!newIDs.equals(this.getSelection())) {
151
            this.setSelectedIDs(newIDs);
152
        }
153
        // ne pas mettre dans le if précédent : on sélectionne 3 lignes puis 2 sont filtrées,
154
        // si l'user clique sur la sélection, newIDs == getSelection(), mais on veut enregistrer
155
        // que l'userID est maintenant juste cette ligne et pas les 3 initiales
156
        // ne changer que si l'utilisateur change directement
157
        if (!this.getModel().isUpdating())
158
            this.setUserSelectedIDs(this.getSelectedIDs());
159
    }
160
 
161
    public void selectID(final int id) {
162
        this.selectIDs(Collections.singletonList(id));
163
    }
164
 
144 ilm 165
    public void selectIDs(final Collection<? extends Number> idsOrig) {
166
        final List<Integer> ids = new ArrayList<Integer>(idsOrig.size());
167
        for (final Number n : idsOrig) {
168
            ids.add((Integer) (n instanceof Integer ? n : n.intValue()));
169
        }
17 ilm 170
 
171
        if (!this.getModel().isUpdating()) {
172
            // sorted asc for use by CollectionUtils.aggregate()
173
            final SortedSet<Integer> newIndexes = new TreeSet<Integer>();
174
            for (final Integer id : ids) {
175
                final int index = indexFromID(id);
176
                // if the id cannot be selected don't add it
177
                if (index != BaseListStateModel.INVALID_INDEX)
178
                    newIndexes.add(index);
179
            }
80 ilm 180
            if (!this.getSelectedIndexesFast().equals(newIndexes)) {
17 ilm 181
                List<int[]> intervals = CollectionUtils.aggregate(new ArrayList<Number>(newIndexes));
182
                if (this.getSelModel().getSelectionMode() != ListSelectionModel.MULTIPLE_INTERVAL_SELECTION && intervals.size() > 1) {
183
                    final String msg = "need MULTIPLE_INTERVAL_SELECTION to select " + CollectionUtils.join(intervals, ", ", new ITransformer<int[], String>() {
184
                        @Override
185
                        public String transformChecked(int[] input) {
186
                            return Arrays.toString(input);
187
                        }
188
                    });
189
                    if (this.isStrict())
190
                        throw new IllegalStateException(msg);
191
                    else {
192
                        final int[] firstInterval = intervals.get(0);
193
                        intervals = Collections.singletonList(firstInterval);
194
                        ids.clear();
195
                        for (int index = firstInterval[0]; index <= firstInterval[1]; index++) {
196
                            ids.add(idFromIndex(index));
197
                        }
198
                        Log.get().info(msg);
199
                    }
200
                }
201
 
202
                if (intervals.size() == 1) {
203
                    // avoid clearSelection() and its fire
204
                    final int[] interval = intervals.get(0);
205
                    this.getSelModel().setSelectionInterval(interval[0], interval[1]);
206
                } else {
156 ilm 207
                    // ATTN minimize calls to the enclosing method as the code below is slow
17 ilm 208
                    this.getSelModel().setValueIsAdjusting(true);
209
                    this.getSelModel().clearSelection();
210
                    for (final int[] interval : intervals) {
211
                        this.getSelModel().addSelectionInterval(interval[0], interval[1]);
212
                    }
213
                    this.getSelModel().setValueIsAdjusting(false);
214
                }
215
            }
216
        }
217
 
218
        // if the ID is not visible that will clear the selection,
219
        // hence make sure the wanted id is indeed id.
220
        // also if this is not strict the ids might be altered
221
        this.setUserSelectedIDs(ids);
222
    }
223
 
224
    // retourne l'ID de la ligne rowIndex à l'écran.
225
    public int idFromIndex(int rowIndex) {
226
        try {
227
            return this.getModel().idFromIndex(rowIndex);
228
        } catch (IndexOutOfBoundsException e) {
229
            return INVALID_ID;
230
        }
231
    }
232
 
233
    // retourne l'index de la ligne d'ID id.
234
    private int indexFromID(int id) {
235
        return this.getModel().indexFromID(id);
236
    }
237
 
238
    // * selections
239
 
240
    private SortedMap<Integer, Integer> getSelection() {
241
        return this.selection;
242
    }
243
 
244
    @Override
245
    public final List<Integer> getSelectedIDs() {
246
        return new ArrayList<Integer>(this.getSelection().values());
247
    }
248
 
249
    /**
250
     * All selected indexes.
251
     *
252
     * @return all selected indexes, ascendant sorted.
253
     */
80 ilm 254
    public final Set<Integer> getSelectedIndexes() {
255
        return Collections.unmodifiableSet(getSelectedIndexesFast());
256
    }
257
 
258
    private Set<Integer> getSelectedIndexesFast() {
17 ilm 259
        return this.getSelection().keySet();
260
    }
261
 
262
    /**
263
     * The currently selected index, that is the lead if it is selected or the first index selected.
264
     *
80 ilm 265
     * @return the currently selected index, {@link BaseListStateModel#INVALID_INDEX} if none, never
266
     *         <code>null</code>.
17 ilm 267
     */
80 ilm 268
    public final Integer getSelectedIndex() {
269
        final Integer res;
17 ilm 270
        if (this.getSelection().isEmpty())
271
            res = INVALID_INDEX;
272
        else {
273
            // getLeadSelectionIndex() renvoie le dernier setSel y compris remove
80 ilm 274
            final Integer lead = this.getSelModel().getLeadSelectionIndex();
275
            if (this.getSelectedIndexesFast().contains(lead))
17 ilm 276
                res = lead;
277
            else
80 ilm 278
                res = this.getSelectedIndexesFast().iterator().next();
17 ilm 279
        }
280
        return res;
281
    }
282
 
283
    /**
284
     * The currently selected id (at the lead index).
285
     *
286
     * @return the currently selected id or INVALID_ID if no selection.
287
     */
288
    @Override
289
    public final int getSelectedID() {
290
        return this.getSelection().isEmpty() ? INVALID_ID : this.getSelection().get(this.getSelectedIndex());
291
    }
292
 
293
    @Override
294
    public final Set<Integer> getUserSelectedIDs() {
295
        return this.userSelectedIDs;
296
    }
297
 
298
    /**
299
     * The desired id. It may not be currently selected but it will be as soon as possible.
300
     *
301
     * @return the desired id or INVALID_ID if no selection.
302
     */
303
    @Override
304
    public final int getUserSelectedID() {
305
        return this.getUserSelectedIDs().size() > 0 ? this.getUserSelectedIDs().iterator().next() : INVALID_ID;
306
    }
307
 
308
    // setters
309
 
310
    private void setSelectedIDs(Map<Integer, Integer> selectedIDs) {
311
        this.selection.clear();
312
        this.selection.putAll(selectedIDs);
313
        this.supp.firePropertyChange("selectedIDs", null, this.getSelectedIDs());
314
        this.supp.firePropertyChange("selectedID", null, this.getSelectedID());
315
        this.supp.firePropertyChange("selectedIndexes", null, this.getSelectedIndexes());
316
        this.supp.firePropertyChange("selectedIndex", null, this.getSelectedIndex());
317
    }
318
 
319
    private void setUserSelectedIDs(Collection<Integer> userSelectedIDs) {
320
        if (!this.userSelectedIDs.equals(new HashSet<Integer>(userSelectedIDs))) {
321
            this.userSelectedIDs.clear();
322
            this.userSelectedIDs.addAll(userSelectedIDs);
323
            this.supp.firePropertyChange("userSelectedIDs", null, this.getUserSelectedIDs());
324
            this.supp.firePropertyChange("userSelectedID", null, this.getUserSelectedID());
325
        }
326
    }
327
 
328
    // *** other props
329
 
330
    public final boolean isUpdating() {
331
        return this.updating;
332
    }
333
 
334
    private final void setUpdating(boolean upd) {
335
        if (upd != this.isUpdating()) {
336
            this.updating = upd;
337
            if (!this.isUpdating()) {
338
                // on finit 1 maj
339
                selectIDs(ListSelectionState.this.getUserSelectedIDs());
340
            }
341
            this.supp.firePropertyChange("updating", null, this.updating);
342
        }
343
    }
344
 
345
    /**
346
     * Whether this is strict when selecting, ie if the asked selection is non-contiguous but the
347
     * selection model is.
348
     *
349
     * @return <code>true</code> if an exception should be thrown, <code>false</code> if the
350
     *         selection should be changed to be compatible with the mode.
351
     */
352
    public final boolean isStrict() {
353
        return this.strict;
354
    }
355
 
356
    public final void setStrict(boolean strict) {
357
        this.strict = strict;
358
    }
359
 
360
    // *** Listeners ***//
361
 
362
    public final void addPropertyChangeListener(String name, final PropertyChangeListener l) {
363
        this.supp.addPropertyChangeListener(name, l);
364
    }
365
 
366
    public final void addPropertyChangeListener(final PropertyChangeListener l) {
367
        this.supp.addPropertyChangeListener(l);
368
    }
369
}