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