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