001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2015-2016 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.ui.components;
018
019import static org.opends.messages.AdminToolMessages.*;
020
021import java.awt.Component;
022import java.awt.Dimension;
023import java.awt.GridBagConstraints;
024import java.awt.GridBagLayout;
025import java.awt.Insets;
026import java.awt.event.ActionEvent;
027import java.awt.event.ActionListener;
028import java.awt.event.MouseAdapter;
029import java.awt.event.MouseEvent;
030import java.util.ArrayList;
031import java.util.Collection;
032
033import javax.swing.Box;
034import javax.swing.JButton;
035import javax.swing.JLabel;
036import javax.swing.JList;
037import javax.swing.JPanel;
038import javax.swing.JScrollPane;
039import javax.swing.ListModel;
040import javax.swing.ListSelectionModel;
041import javax.swing.event.ListDataEvent;
042import javax.swing.event.ListDataListener;
043import javax.swing.event.ListSelectionEvent;
044import javax.swing.event.ListSelectionListener;
045
046import org.opends.guitools.controlpanel.datamodel.SortableListModel;
047import org.opends.guitools.controlpanel.util.Utilities;
048
049/**
050 * This component displays three list (one available list and two selected
051 * lists) with some buttons to move the components of one list to the other.
052 *
053 * @param <T> the type of the objects in the list.
054 */
055public class DoubleAddRemovePanel<T> extends JPanel
056{
057  private static final long serialVersionUID = 6881453848780359594L;
058  private final SortableListModel<T> availableListModel;
059  private final SortableListModel<T> selectedListModel1;
060  private final SortableListModel<T> selectedListModel2;
061  private final JLabel selectedLabel1;
062  private final JLabel selectedLabel2;
063  private final JLabel availableLabel;
064  private final JButton add1;
065  private final JButton remove1;
066  private final JButton add2;
067  private final JButton remove2;
068  private final JButton addAll1;
069  private final JButton removeAll1;
070  private final JButton addAll2;
071  private final JButton removeAll2;
072  private final JScrollPane availableScroll;
073  private final JScrollPane selectedScroll1;
074  private final JScrollPane selectedScroll2;
075  private final JList availableList;
076  private final JList<T> selectedList1;
077  private final JList<T> selectedList2;
078  private final Class<T> theClass;
079  private final Collection<T> unmovableItems = new ArrayList<>();
080  private boolean ignoreListEvents;
081
082  /**
083   * Mask used as display option.  If the provided display options contain
084   * this mask, the panel will display the remove all button.
085   */
086  private static final int DISPLAY_REMOVE_ALL = 0x001;
087
088  /**
089   * Mask used as display option.  If the provided display options contain
090   * this mask, the panel will display the add all button.
091   */
092  private static final int DISPLAY_ADD_ALL = 0x010;
093
094  /**
095   * Constructor of the double add remove panel allowing the user to provide
096   * some display options.
097   * The class is required to avoid warnings in compilation.
098   * @param displayOptions the display options.
099   * @param theClass the class of the objects in the panel.
100   */
101  public DoubleAddRemovePanel(int displayOptions, Class<T> theClass)
102  {
103    super(new GridBagLayout());
104    setOpaque(false);
105    this.theClass = theClass;
106    GridBagConstraints gbc = new GridBagConstraints();
107    gbc.gridx = 0;
108    gbc.gridy = 0;
109    gbc.weightx = 0.0;
110    gbc.weighty = 0.0;
111    gbc.gridwidth = 1;
112    gbc.gridheight = 1;
113    gbc.fill = GridBagConstraints.HORIZONTAL;
114    gbc.anchor = GridBagConstraints.WEST;
115
116    availableLabel = Utilities.createDefaultLabel(
117        INFO_CTRL_PANEL_AVAILABLE_LABEL.get());
118    add(availableLabel, gbc);
119    gbc.gridx = 2;
120    selectedLabel1 = Utilities.createDefaultLabel(
121        INFO_CTRL_PANEL_SELECTED_LABEL.get());
122    add(selectedLabel1, gbc);
123    gbc.gridy ++;
124
125    ListDataListener listDataListener = new ListDataListener()
126    {
127      @Override
128      public void intervalRemoved(ListDataEvent ev)
129      {
130        listSelectionChanged();
131      }
132
133      @Override
134      public void intervalAdded(ListDataEvent ev)
135      {
136        listSelectionChanged();
137      }
138
139      @Override
140      public void contentsChanged(ListDataEvent ev)
141      {
142        listSelectionChanged();
143      }
144    };
145    MouseAdapter doubleClickListener = new MouseAdapter()
146    {
147      @Override
148      public void mouseClicked(MouseEvent e) {
149        if (isEnabled() && e.getClickCount() == 2)
150        {
151          if (e.getSource() == availableList)
152          {
153            if (availableList.getSelectedValue() != null)
154            {
155              addClicked(selectedListModel1);
156            }
157          }
158          else if (e.getSource() == selectedList1)
159          {
160            if (selectedList1.getSelectedValue() != null)
161            {
162              remove1Clicked();
163            }
164          }
165          else if (e.getSource() == selectedList2
166              && selectedList2.getSelectedValue() != null)
167          {
168            remove2Clicked();
169          }
170        }
171      }
172    };
173
174
175    availableListModel = new SortableListModel<>();
176    availableListModel.addListDataListener(listDataListener);
177    availableList = new JList<>();
178    availableList.setModel(availableListModel);
179    availableList.setVisibleRowCount(15);
180    availableList.addMouseListener(doubleClickListener);
181
182    selectedListModel1 = new SortableListModel<>();
183    selectedListModel1.addListDataListener(listDataListener);
184    selectedList1 = new JList<>();
185    selectedList1.setModel(selectedListModel1);
186    selectedList1.setVisibleRowCount(7);
187    selectedList1.addMouseListener(doubleClickListener);
188
189    selectedListModel2 = new SortableListModel<>();
190    selectedListModel2.addListDataListener(listDataListener);
191    selectedList2 = new JList<>();
192    selectedList2.setModel(selectedListModel2);
193    selectedList2.setVisibleRowCount(7);
194    selectedList2.addMouseListener(doubleClickListener);
195
196    gbc.weighty = 1.0;
197    gbc.weightx = 1.0;
198    gbc.gridheight = 7;
199    displayOptions &= DISPLAY_ADD_ALL;
200    if (displayOptions != 0)
201    {
202      gbc.gridheight += 2;
203    }
204    // FIXME how can this be any different than 0? Ditto everywhere else down below
205    displayOptions &= DISPLAY_REMOVE_ALL;
206    if (displayOptions != 0)
207    {
208      gbc.gridheight += 2;
209    }
210    int listGridY = gbc.gridy;
211    gbc.gridx = 0;
212    gbc.insets.top = 5;
213    availableScroll = Utilities.createScrollPane(availableList);
214    gbc.fill = GridBagConstraints.BOTH;
215    add(availableScroll, gbc);
216
217    gbc.gridx = 1;
218    gbc.gridheight = 1;
219    gbc.weightx = 0.0;
220    gbc.weighty = 0.0;
221    gbc.fill = GridBagConstraints.HORIZONTAL;
222    add1 = Utilities.createButton(INFO_CTRL_PANEL_ADDREMOVE_ADD_BUTTON.get());
223    add1.setOpaque(false);
224    add1.addActionListener(new ActionListener()
225    {
226      @Override
227      public void actionPerformed(ActionEvent ev)
228      {
229        addClicked(selectedListModel1);
230      }
231    });
232    gbc.insets = new Insets(5, 5, 0, 5);
233    add(add1, gbc);
234
235    displayOptions &= DISPLAY_ADD_ALL;
236    if (displayOptions != 0)
237    {
238      addAll1 = Utilities.createButton(
239          INFO_CTRL_PANEL_ADDREMOVE_ADD_ALL_BUTTON.get());
240      addAll1.setOpaque(false);
241      addAll1.addActionListener(new ActionListener()
242      {
243        @Override
244        public void actionPerformed(ActionEvent ev)
245        {
246          moveAll(availableListModel, selectedListModel1);
247        }
248      });
249      gbc.gridy ++;
250      add(addAll1, gbc);
251    }
252    else
253    {
254      addAll1 = null;
255    }
256
257    remove1 = Utilities.createButton(
258        INFO_CTRL_PANEL_ADDREMOVE_REMOVE_BUTTON.get());
259    remove1.setOpaque(false);
260    remove1.addActionListener(new ActionListener()
261    {
262      @Override
263      public void actionPerformed(ActionEvent ev)
264      {
265        remove1Clicked();
266      }
267    });
268    gbc.gridy ++;
269    gbc.insets.top = 10;
270    add(remove1, gbc);
271
272    displayOptions &= DISPLAY_REMOVE_ALL;
273    if (displayOptions != 0)
274    {
275      removeAll1 = Utilities.createButton(
276          INFO_CTRL_PANEL_ADDREMOVE_REMOVE_ALL_BUTTON.get());
277      removeAll1.setOpaque(false);
278      removeAll1.addActionListener(new ActionListener()
279      {
280        @Override
281        public void actionPerformed(ActionEvent ev)
282        {
283          moveAll(selectedListModel1, availableListModel);
284        }
285      });
286      gbc.gridy ++;
287      gbc.insets.top = 5;
288      add(removeAll1, gbc);
289    }
290    else
291    {
292      removeAll1 = null;
293    }
294
295
296    gbc.weighty = 1.0;
297    gbc.insets = new Insets(0, 0, 0, 0);
298    gbc.gridy ++;
299    gbc.gridheight = 1;
300    gbc.fill = GridBagConstraints.VERTICAL;
301    add(Box.createVerticalGlue(), gbc);
302
303    gbc.gridy += 2;
304    gbc.gridx = 1;
305    gbc.gridheight = 1;
306    gbc.weightx = 0.0;
307    gbc.weighty = 0.0;
308    gbc.fill = GridBagConstraints.HORIZONTAL;
309    add2 = Utilities.createButton(INFO_CTRL_PANEL_ADDREMOVE_ADD_BUTTON.get());
310    add2.setOpaque(false);
311    add2.addActionListener(new ActionListener()
312    {
313      @Override
314      public void actionPerformed(ActionEvent ev)
315      {
316        addClicked(selectedListModel2);
317      }
318    });
319    gbc.insets = new Insets(5, 5, 0, 5);
320    add(add2, gbc);
321
322    displayOptions &= DISPLAY_ADD_ALL;
323    if (displayOptions != 0)
324    {
325      addAll2 = Utilities.createButton(
326          INFO_CTRL_PANEL_ADDREMOVE_ADD_ALL_BUTTON.get());
327      addAll2.setOpaque(false);
328      addAll2.addActionListener(new ActionListener()
329      {
330        @Override
331        public void actionPerformed(ActionEvent ev)
332        {
333          moveAll(availableListModel, selectedListModel2);
334        }
335      });
336      gbc.gridy ++;
337      add(addAll2, gbc);
338    }
339    else
340    {
341      addAll2 = null;
342    }
343
344    remove2 = Utilities.createButton(
345        INFO_CTRL_PANEL_ADDREMOVE_REMOVE_BUTTON.get());
346    remove2.setOpaque(false);
347    remove2.addActionListener(new ActionListener()
348    {
349      @Override
350      public void actionPerformed(ActionEvent ev)
351      {
352        remove2Clicked();
353      }
354    });
355    gbc.gridy ++;
356    gbc.insets.top = 10;
357    add(remove2, gbc);
358
359    displayOptions &= DISPLAY_REMOVE_ALL;
360    if (displayOptions != 0)
361    {
362      removeAll2 = Utilities.createButton(
363          INFO_CTRL_PANEL_ADDREMOVE_REMOVE_ALL_BUTTON.get());
364      removeAll2.setOpaque(false);
365      removeAll2.addActionListener(new ActionListener()
366      {
367        @Override
368        public void actionPerformed(ActionEvent ev)
369        {
370          moveAll(selectedListModel2, availableListModel);
371        }
372      });
373      gbc.gridy ++;
374      gbc.insets.top = 5;
375      add(removeAll2, gbc);
376    }
377    else
378    {
379      removeAll2 = null;
380    }
381
382
383    gbc.weighty = 1.0;
384    gbc.insets = new Insets(0, 0, 0, 0);
385    gbc.gridy ++;
386    gbc.gridheight = 1;
387    gbc.fill = GridBagConstraints.VERTICAL;
388    add(Box.createVerticalGlue(), gbc);
389
390    gbc.weightx = 1.0;
391    gbc.insets = new Insets(5, 0, 0, 0);
392    gbc.gridheight = 3;
393    displayOptions &= DISPLAY_ADD_ALL;
394    if (displayOptions != 0)
395    {
396      gbc.gridheight ++;
397    }
398    displayOptions &= DISPLAY_REMOVE_ALL;
399    if (displayOptions != 0)
400    {
401      gbc.gridheight ++;
402    }
403    gbc.gridy = listGridY;
404    gbc.gridx = 2;
405    gbc.fill = GridBagConstraints.BOTH;
406    selectedScroll1 = Utilities.createScrollPane(selectedList1);
407    gbc.weighty = 1.0;
408    add(selectedScroll1, gbc);
409
410    gbc.gridy += gbc.gridheight;
411    gbc.gridheight = 1;
412    gbc.weighty = 0.0;
413    gbc.insets.top = 10;
414    gbc.fill = GridBagConstraints.HORIZONTAL;
415    selectedLabel2 = Utilities.createDefaultLabel(
416        INFO_CTRL_PANEL_SELECTED_LABEL.get());
417    add(selectedLabel2, gbc);
418
419    gbc.weightx = 1.0;
420    gbc.insets = new Insets(5, 0, 0, 0);
421    gbc.gridheight = 3;
422    displayOptions &= DISPLAY_ADD_ALL;
423    if (displayOptions != 0)
424    {
425      gbc.gridheight ++;
426    }
427    displayOptions &= DISPLAY_REMOVE_ALL;
428    if (displayOptions != 0)
429    {
430      gbc.gridheight ++;
431    }
432    gbc.gridy ++;
433    gbc.fill = GridBagConstraints.BOTH;
434    selectedScroll2 = Utilities.createScrollPane(selectedList2);
435    gbc.weighty = 1.0;
436    add(selectedScroll2, gbc);
437
438
439    selectedList1.getSelectionModel().setSelectionMode(
440        ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
441    ListSelectionListener listener = new ListSelectionListener()
442    {
443      @Override
444      public void valueChanged(ListSelectionEvent ev)
445      {
446        listSelectionChanged();
447      }
448    };
449    selectedList1.getSelectionModel().addListSelectionListener(listener);
450    selectedList2.getSelectionModel().setSelectionMode(
451        ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
452    selectedList2.getSelectionModel().addListSelectionListener(listener);
453    availableList.getSelectionModel().setSelectionMode(
454        ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
455    availableList.getSelectionModel().addListSelectionListener(listener);
456
457    add1.setEnabled(false);
458    remove1.setEnabled(false);
459
460    add2.setEnabled(false);
461    remove2.setEnabled(false);
462
463    // Set preferred size for the scroll panes.
464    Component comp =
465      availableList.getCellRenderer().getListCellRendererComponent(
466          availableList,
467        "The cell that we want to display", 0, true, true);
468    Dimension d = new Dimension(comp.getPreferredSize().width,
469        availableScroll.getPreferredSize().height);
470    availableScroll.setPreferredSize(d);
471    d = new Dimension(comp.getPreferredSize().width,
472        selectedScroll1.getPreferredSize().height);
473    selectedScroll1.setPreferredSize(d);
474    selectedScroll2.setPreferredSize(d);
475  }
476
477  @Override
478  public void setEnabled(boolean enable)
479  {
480    super.setEnabled(enable);
481
482    selectedLabel1.setEnabled(enable);
483    selectedLabel2.setEnabled(enable);
484    availableLabel.setEnabled(enable);
485    availableList.setEnabled(enable);
486    selectedList1.setEnabled(enable);
487    selectedList2.setEnabled(enable);
488    availableScroll.setEnabled(enable);
489    selectedScroll2.setEnabled(enable);
490    selectedScroll2.setEnabled(enable);
491
492    listSelectionChanged();
493  }
494
495  /**
496   * Returns the available label contained in the panel.
497   * @return the available label contained in the panel.
498   */
499  public JLabel getAvailableLabel()
500  {
501    return availableLabel;
502  }
503
504  /**
505   * Returns the list of elements in the available list.
506   * @return the list of elements in the available list.
507   */
508  public SortableListModel<T> getAvailableListModel()
509  {
510    return availableListModel;
511  }
512
513  /**
514   * Returns the first selected label contained in the panel.
515   * @return the first selected label contained in the panel.
516   */
517  public JLabel getSelectedLabel1()
518  {
519    return selectedLabel1;
520  }
521
522  /**
523   * Returns the list of elements in the first selected list.
524   * @return the list of elements in the first selected list.
525   */
526  public SortableListModel<T> getSelectedListModel1()
527  {
528    return selectedListModel1;
529  }
530
531  /**
532   * Returns the second selected label contained in the panel.
533   * @return the second selected label contained in the panel.
534   */
535  public JLabel getSelectedLabel2()
536  {
537    return selectedLabel2;
538  }
539
540  /**
541   * Returns the list of elements in the second selected list.
542   * @return the list of elements in the second selected list.
543   */
544  public SortableListModel<T> getSelectedListModel2()
545  {
546    return selectedListModel2;
547  }
548
549  private void listSelectionChanged()
550  {
551    if (ignoreListEvents)
552    {
553      return;
554    }
555    ignoreListEvents = true;
556
557    JList[] lists = {availableList, selectedList1, selectedList2};
558    for (JList<T> list : lists)
559    {
560      for (T element : unmovableItems)
561      {
562        int[] indexes = list.getSelectedIndices();
563        if (indexes != null)
564        {
565          for (int index : indexes)
566          {
567            // This check is necessary since the selection model might not
568            // be in sync with the list model.
569            if (selectionAndListModelAreInSync(list, element, index))
570            {
571              list.getSelectionModel().removeIndexInterval(index, index);
572            }
573          }
574        }
575      }
576    }
577
578    ignoreListEvents = false;
579    add1.setEnabled(isEnabled(availableList, availableListModel));
580    add2.setEnabled(add1.isEnabled());
581    remove1.setEnabled(isEnabled(selectedList1, selectedListModel1));
582    remove2.setEnabled(isEnabled(selectedList2, selectedListModel2));
583
584    if (addAll1 != null)
585    {
586      addAll1.setEnabled(isEnabled(availableListModel));
587      addAll2.setEnabled(addAll1.isEnabled());
588    }
589    if (removeAll1 != null)
590    {
591      removeAll1.setEnabled(isEnabled(selectedListModel1));
592    }
593    if (removeAll2 != null)
594    {
595      removeAll2.setEnabled(isEnabled(selectedListModel2));
596    }
597  }
598
599  private boolean selectionAndListModelAreInSync(JList<T> list, T element, int index)
600  {
601    final ListModel<T> listModel = list.getModel();
602    return index < listModel.getSize()
603        && listModel.getElementAt(index).equals(element);
604  }
605
606  private boolean isEnabled(JList<T> list, SortableListModel<T> model)
607  {
608    int index = list.getSelectedIndex();
609    return index != -1 && index < model.getSize() && isEnabled();
610  }
611
612  private boolean isEnabled(SortableListModel<T> model)
613  {
614    boolean onlyUnmovable = unmovableItems.containsAll(model.getData());
615    return model.getSize() > 0 && isEnabled() && !onlyUnmovable;
616  }
617
618  /**
619   * Returns the available list.
620   * @return the available list.
621   */
622  public JList getAvailableList()
623  {
624    return availableList;
625  }
626
627  /**
628   * Returns the first selected list.
629   * @return the first selected list.
630   */
631  public JList<T> getSelectedList1()
632  {
633    return selectedList1;
634  }
635
636  /**
637   * Returns the second selected list.
638   * @return the second selected list.
639   */
640  public JList<T> getSelectedList2()
641  {
642    return selectedList2;
643  }
644
645  private void addClicked(SortableListModel<T> selectedListModel)
646  {
647    for (Object selectedObject : availableList.getSelectedValuesList())
648    {
649      T value = DoubleAddRemovePanel.this.theClass.cast(selectedObject);
650      selectedListModel.add(value);
651      availableListModel.remove(value);
652    }
653    selectedListModel.fireContentsChanged(selectedListModel, 0, selectedListModel.getSize());
654    availableListModel.fireContentsChanged(availableListModel, 0, availableListModel.getSize());
655  }
656
657  private void remove1Clicked()
658  {
659    removeClicked(selectedListModel1, selectedList1);
660  }
661
662  private void remove2Clicked()
663  {
664    removeClicked(selectedListModel2, selectedList2);
665  }
666
667  private void removeClicked(SortableListModel<T> selectedListModel, JList<T> selectedList)
668  {
669    for (T value : selectedList.getSelectedValuesList())
670    {
671      availableListModel.add(value);
672      selectedListModel.remove(value);
673    }
674    selectedListModel.fireContentsChanged(selectedListModel, 0, selectedListModel.getSize());
675    availableListModel.fireContentsChanged(availableListModel, 0, availableListModel.getSize());
676  }
677
678  /**
679   * Sets the list of items that cannot be moved from one list to the others.
680   * @param unmovableItems the list of items that cannot be moved from one
681   * list to the others.
682   */
683  public void setUnmovableItems(Collection<T> unmovableItems)
684  {
685    this.unmovableItems.clear();
686    this.unmovableItems.addAll(unmovableItems);
687  }
688
689  private void moveAll(SortableListModel<T> fromModel,
690      SortableListModel<T> toModel)
691  {
692    Collection<T> toKeep = fromModel.getData();
693    toKeep.retainAll(unmovableItems);
694    Collection<T> toMove = fromModel.getData();
695    toMove.removeAll(unmovableItems);
696    toModel.addAll(toMove);
697    fromModel.clear();
698    fromModel.addAll(toKeep);
699    fromModel.fireContentsChanged(selectedListModel1, 0,
700        selectedListModel1.getSize());
701    toModel.fireContentsChanged(availableListModel, 0,
702        availableListModel.getSize());
703  }
704}