View Javadoc

1   /*
2   
3       dsh-curate  Interfaces for curating collections quickly.
4       Copyright (c) 2007-2013 held jointly by the individual authors.
5   
6       This library is free software; you can redistribute it and/or modify it
7       under the terms of the GNU Lesser General Public License as published
8       by the Free Software Foundation; either version 3 of the License, or (at
9       your option) any later version.
10  
11      This library is distributed in the hope that it will be useful, but WITHOUT
12      ANY WARRANTY; with out even the implied warranty of MERCHANTABILITY or
13      FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14      License for more details.
15  
16      You should have received a copy of the GNU Lesser General Public License
17      along with this library;  if not, write to the Free Software Foundation,
18      Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA.
19  
20      > http://www.fsf.org/licensing/licenses/lgpl.html
21      > http://www.opensource.org/licenses/lgpl-license.php
22  
23  */
24  package org.dishevelled.curate.impl;
25  
26  import java.awt.GridLayout;
27  
28  import java.awt.event.ActionEvent;
29  import java.awt.event.ActionListener;
30  import java.awt.event.KeyAdapter;
31  import java.awt.event.KeyEvent;
32  import java.awt.event.MouseAdapter;
33  import java.awt.event.MouseEvent;
34  
35  import java.util.Collection;
36  import java.util.Collections;
37  
38  import javax.swing.AbstractAction;
39  import javax.swing.Action;
40  import javax.swing.JLabel;
41  import javax.swing.JList;
42  import javax.swing.JPanel;
43  import javax.swing.JPopupMenu;
44  import javax.swing.JScrollPane;
45  import javax.swing.KeyStroke;
46  import javax.swing.Timer;
47  
48  import javax.swing.event.ListSelectionEvent;
49  import javax.swing.event.ListSelectionListener;
50  
51  import ca.odell.glazedlists.EventList;
52  import ca.odell.glazedlists.GlazedLists;
53  
54  import ca.odell.glazedlists.event.ListEvent;
55  import ca.odell.glazedlists.event.ListEventListener;
56  
57  import ca.odell.glazedlists.swing.EventListModel;
58  import ca.odell.glazedlists.swing.EventSelectionModel;
59  
60  import org.dishevelled.curate.CullView;
61  
62  import org.dishevelled.iconbundle.tango.TangoProject;
63  
64  import org.dishevelled.identify.IdentifiableAction;
65  import org.dishevelled.identify.IdMenuItem;
66  
67  import org.dishevelled.layout.LabelFieldPanel;
68  
69  /**
70   * Cull panel.
71   *
72   * @param <E> input and result collections element type
73   * @author  Michael Heuer
74   * @version $Revision$ $Date$
75   */
76  public final class CullPanel<E>
77      extends JPanel
78      implements CullView<E>
79  {
80      /** Default assist rate (in ms). */
81      private static final int DEFAULT_ASSIST_RATE = 1000;
82  
83      /** Input collection. */
84      private Collection<E> input;
85  
86      /** List of remaining elements. */
87      private EventList<E> remaining;
88  
89      /** List of selected remaining elements. */
90      private EventList<E> selectedRemaining;
91  
92      /** Swing list of remaining elements. */
93      private final JList remainingList;
94  
95      /** Label for count of remaining elements. */
96      private final JLabel remainingLabel;
97  
98      /** List event listener for updating remaining label. */
99      private final ListEventListener<E> remainingLabelListener;
100 
101     /** List of removed elements. */
102     private EventList<E> removed;
103 
104     /** Swing list of removed elements. */
105     private final JList removedList;
106 
107     /** Label for number of removed elements. */
108     private final JLabel removedLabel;
109 
110     /** List event listener for updating removed label. */
111     private final ListEventListener<E> removedLabelListener;
112 
113     /** Assist. */
114     private final Assist assist;
115 
116     /** Remove action. */
117     private final IdentifiableAction removeAction;
118 
119     /** Remove all action. */
120     private final Action removeAllAction;
121 
122     /** Start assist action. */
123     private final IdentifiableAction startAssistAction;
124 
125     /** Stop assist action. */
126     private final IdentifiableAction stopAssistAction;
127 
128     /** Toggle assist action. */
129     private final Action toggleAssistAction;
130 
131     /** Context menu. */
132     private final JPopupMenu contextMenu;
133 
134 
135     /**
136      * Create a new cull panel.
137      */
138     public CullPanel()
139     {
140         super();
141 
142         input = Collections.emptyList();
143         remaining = GlazedLists.eventList(input);
144         removed = GlazedLists.eventList(input);
145 
146         remainingLabel = new JLabel(" - ");
147         remainingLabelListener = new ListEventListener<E>()
148             {
149                 /** {@inheritDoc} */
150                 public void listChanged(final ListEvent<E> event)
151                 {
152                     StringBuffer sb = new StringBuffer();
153                     sb.append(remaining.size());
154                     sb.append(" of ");
155                     sb.append(input.size());
156                     remainingLabel.setText(sb.toString());
157                 }
158             };
159         remainingList = new JList(new EventListModel<E>(remaining));
160 
161         EventSelectionModel<E> remainingSelectionModel = new EventSelectionModel<E>(remaining);
162         remainingSelectionModel.setSelectionMode(EventSelectionModel.MULTIPLE_INTERVAL_SELECTION);
163         remainingList.setSelectionModel(remainingSelectionModel);
164         selectedRemaining = remainingSelectionModel.getSelected();
165 
166         removedLabel = new JLabel(" - ");
167         removedLabelListener = new ListEventListener<E>()
168             {
169                 /** {@inheritDoc} */
170                 public void listChanged(final ListEvent<E> event)
171                 {
172                     StringBuffer sb = new StringBuffer();
173                     sb.append(removed.size());
174                     sb.append(" of ");
175                     sb.append(input.size());
176                     removedLabel.setText(sb.toString());
177                 }
178             };
179         removedList = new JList(new EventListModel<E>(removed));
180 
181         removeAction = new IdentifiableAction("Remove", TangoProject.LIST_REMOVE)
182             {
183                 /** {@inheritDoc} */
184                 public void actionPerformed(final ActionEvent event)
185                 {
186                     EventSelectionModel<E> remainingSelectionModel = (EventSelectionModel<E>) remainingList.getSelectionModel();
187                     int maxSelectionIndex = Math.min(remainingSelectionModel.getMaxSelectionIndex() + 1, remaining.size() - 1);
188                     int selectionSize = selectedRemaining.size();
189                     int newSelectionIndex = maxSelectionIndex - selectionSize;
190                     removed.addAll(selectedRemaining);
191                     remaining.removeAll(selectedRemaining);
192                     remainingSelectionModel.setSelectionInterval(newSelectionIndex, newSelectionIndex);
193                     remainingList.ensureIndexIsVisible(newSelectionIndex);
194                     if (assist.isRunning())
195                     {
196                         assist.stop();
197                         assist.start();
198                     }
199                 }
200             };
201 
202         removeAllAction = new AbstractAction("Remove all")
203             {
204                 /** {@inheritDoc} */
205                 public void actionPerformed(final ActionEvent event)
206                 {
207                     if (assist.isRunning())
208                     {
209                         assist.stop();
210                     }
211                     removed.addAll(remaining);
212                     remaining.clear();
213                 }
214             };
215 
216         assist = new Assist(DEFAULT_ASSIST_RATE);
217 
218         startAssistAction = new IdentifiableAction("Start assist", TangoProject.MEDIA_PLAYBACK_START)
219             {
220                 /** {@inheritDoc} */
221                 public void actionPerformed(final ActionEvent event)
222                 {
223                     if (!assist.isRunning())
224                     {
225                         assist.start();
226                     }
227                 }
228             };
229 
230         // ...or compound play+pause button?
231         stopAssistAction = new IdentifiableAction("Stop assist", TangoProject.MEDIA_PLAYBACK_STOP)
232             {
233                 /** {@inheritDoc} */
234                 public void actionPerformed(final ActionEvent event)
235                 {
236                     if (assist.isRunning())
237                     {
238                         assist.stop();
239                     }
240                 }
241             };
242         stopAssistAction.setEnabled(false);
243 
244         toggleAssistAction = new AbstractAction("Toggle assist")
245             {
246                 /** {@inheritDoc} */
247                 public void actionPerformed(final ActionEvent event)
248                 {
249                     if (assist.isRunning())
250                     {
251                         assist.stop();
252                     }
253                     else
254                     {
255                         assist.start();
256                     }
257                 }
258             };
259 
260         remainingList.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "remove");
261         remainingList.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "remove");
262         remainingList.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), "remove");
263         remainingList.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "remove");
264         remainingList.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "toggleAssist");
265         remainingList.getActionMap().put("remove", removeAction);
266         remainingList.getActionMap().put("toggleAssist", toggleAssistAction);
267 
268         // up key should stop assist
269         remainingList.addKeyListener(new KeyAdapter()
270             {
271                 /** {@inheritDoc} */
272                 public void keyPressed(final KeyEvent event)
273                 {
274                     if (assist.isRunning())
275                     {
276                         if (KeyEvent.VK_UP == event.getKeyCode())
277                         {
278                             assist.stop();
279                         }
280                     }
281                 }
282             });
283 
284         // multiple value selections should stop assist
285         remainingList.addListSelectionListener(new ListSelectionListener()
286             {
287                 /** {@inheritDoc} */
288                 public void valueChanged(final ListSelectionEvent event)
289                 {
290                     if (assist.isRunning())
291                     {
292                         if (remainingList.getSelectedIndices().length > 1)
293                         {
294                             assist.stop();
295                         }
296                     }
297                 }
298             });
299 
300         // TODO:  down should restart assist?
301         // TODO:  adapt assist rate to combined delete + down keystroke rate
302         contextMenu = new JPopupMenu();
303         contextMenu.add(new IdMenuItem(removeAction));
304         contextMenu.add(removeAllAction);
305         contextMenu.addSeparator();
306         contextMenu.add(new IdMenuItem(startAssistAction));
307         // stop action will never be enabled on popup trigger
308         //contextMenu.add(new IdMenuItem(stopAssistAction));
309 
310         remainingList.addMouseListener(new MouseAdapter()
311             {
312                 /** {@inheritDoc} */
313                 public void mousePressed(final MouseEvent event) {
314                     if (assist.isRunning())
315                     {
316                         assist.stop();
317                     }
318                     if (event.isPopupTrigger())
319                     {
320                         showContextMenu(event);
321                     }
322                 }
323 
324                 /** {@inheritDoc} */
325                 public void mouseReleased(final MouseEvent event) {
326                     if (event.isPopupTrigger())
327                     {
328                         showContextMenu(event);
329                     }
330                 }
331 
332                 /**
333                  * Show context menu.
334                  */
335                 private void showContextMenu(final MouseEvent event)
336                 {
337                     contextMenu.show(event.getComponent(), event.getX(), event.getY());
338                 }
339             });
340 
341         layoutComponents();
342     }
343 
344 
345     /**
346      * Layout components.
347      */
348     private void layoutComponents()
349     {
350         LabelFieldPanel west = new LabelFieldPanel();
351         west.addField("Remaining:", remainingLabel);
352         west.addFinalField(new JScrollPane(remainingList));
353 
354         LabelFieldPanel east = new LabelFieldPanel();
355         east.addField("Removed:", removedLabel);
356         east.addFinalField(new JScrollPane(removedList));
357 
358         setLayout(new GridLayout(1, 2, 10, 0));
359         add(west);
360         add(east);
361         // TODO:  add a user-definable "details..." panel center
362     }
363 
364     /** {@inheritDoc} */
365     public void setInput(final Collection<E> input)
366     {
367         if (input == null)
368         {
369             throw new IllegalArgumentException("input must not be null");
370         }
371         this.input = input;
372 
373         // see http://glazedlists.dev.java.net/issues/show_bug.cgi?id=419
374         try
375         {
376             remaining.removeListEventListener(remainingLabelListener);
377         }
378         catch (IllegalArgumentException e)
379         {
380             // ignore
381         }
382         try
383         {
384             removed.removeListEventListener(removedLabelListener);
385         }
386         catch (IllegalArgumentException e)
387         {
388             // ignore
389         }
390 
391         remaining = GlazedLists.eventList(input);
392         removed = GlazedLists.eventList(Collections.<E>emptyList());
393 
394         remainingList.setModel(new EventListModel<E>(remaining));
395         removedList.setModel(new EventListModel<E>(removed));
396 
397         EventSelectionModel<E> remainingSelectionModel = new EventSelectionModel<E>(remaining);
398         remainingList.setSelectionModel(remainingSelectionModel);
399         remainingSelectionModel.setSelectionMode(EventSelectionModel.MULTIPLE_INTERVAL_SELECTION);
400         selectedRemaining = remainingSelectionModel.getSelected();
401 
402         remaining.addListEventListener(remainingLabelListener);
403         removed.addListEventListener(removedLabelListener);
404 
405         // mock change event to refresh labels
406         remainingLabelListener.listChanged(null);
407         removedLabelListener.listChanged(null);
408     }
409 
410     /** {@inheritDoc} */
411     public Collection<E> getRemaining()
412     {
413         return Collections.unmodifiableList(remaining);
414     }
415 
416     /** {@inheritDoc} */
417     public Collection<E> getRemoved()
418     {
419         return Collections.unmodifiableList(removed);
420     }
421 
422     /**
423      * Return the remove action for this cull panel.
424      * The remove action will not be null.
425      *
426      * @return the remove action for this cull panel
427      */
428     IdentifiableAction getRemoveAction()
429     {
430         return removeAction;
431     }
432 
433     /**
434      * Return the remove all action for this cull panel.
435      * The remove all action will not be null.
436      *
437      * @return the remove action for this cull panel
438      */
439     Action getRemoveAllAction()
440     {
441         return removeAllAction;
442     }
443 
444     /**
445      * Return the start assist action for this cull panel.
446      * The start assist action will not be null.
447      *
448      * @return the start assist action for this cull panel
449      */
450     IdentifiableAction getStartAssistAction()
451     {
452         return startAssistAction;
453     }
454 
455     /**
456      * Return the stop assist action for this cull panel.
457      * The stop assist action will not be null.
458      *
459      * @return the stop assist action for this cull panel
460      */
461     IdentifiableAction getStopAssistAction()
462     {
463         return stopAssistAction;
464     }
465 
466     /**
467      * Assist.
468      */
469     private class Assist
470     {
471         /** Rate (in ms) at which new selection change events are triggered. */
472         private int rate;
473 
474         /** True if assist is running. */
475         private boolean running;
476 
477         /** Timer. */
478         private Timer timer;
479 
480 
481         /**
482          * Create a new assist with the specified rate.
483          *
484          * @param rate rate (in ms) at which new selection change events are triggered
485          */
486         Assist(final int rate)
487         {
488             this.rate = rate;
489             this.running = false;
490             timer = new Timer(0, new ActionListener()
491                 {
492                     /** {@inheritDoc} */
493                     public void actionPerformed(final ActionEvent event)
494                     {
495                         EventSelectionModel<E> remainingSelectionModel = (EventSelectionModel<E>) remainingList.getSelectionModel();
496                         int maxSelectionIndex = Math.min(remaining.size() - 1, remainingSelectionModel.getMaxSelectionIndex() + 1);
497                         remainingSelectionModel.setSelectionInterval(maxSelectionIndex, maxSelectionIndex);
498                         remainingList.ensureIndexIsVisible(maxSelectionIndex);
499                     }
500                 });
501             timer.setRepeats(true);
502         }
503 
504 
505         /**
506          * Set the rate (in ms) at which new selection change events are triggered to <code>rate</code>.
507          *
508          * @param rate rate (in ms) at which new seleciton change events are triggered
509          */
510         void setRate(final int rate)
511         {
512             this.rate = rate;
513         }
514 
515         /**
516          * Return true if this assist is running.
517          *
518          * @return true if this assist is running
519          */
520         boolean isRunning()
521         {
522             return running;
523         }
524 
525         /**
526          * Start generating new selection change events.
527          */
528         void start()
529         {
530             running = true;
531             timer.setDelay(rate);
532             timer.setInitialDelay(rate);
533             startAssistAction.setEnabled(false);
534             stopAssistAction.setEnabled(true);
535             timer.start();
536         }
537 
538         /**
539          * Stop generating new selection change events.
540          */
541         void stop()
542         {
543             running = false;
544             startAssistAction.setEnabled(true);
545             stopAssistAction.setEnabled(false);
546             timer.stop();
547         }
548     }
549 }