001    /*
002     * $Id: AddePreferences.java,v 1.14 2012/02/19 17:35:48 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    package edu.wisc.ssec.mcidasv.servermanager;
031    
032    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
033    
034    import java.awt.Dimension;
035    import java.awt.Insets;
036    import java.awt.Point;
037    import java.awt.event.ActionEvent;
038    import java.awt.event.ActionListener;
039    import java.util.HashSet;
040    import java.util.LinkedHashMap;
041    import java.util.List;
042    import java.util.Map;
043    import java.util.Set;
044    import java.util.Map.Entry;
045    
046    import javax.swing.Icon;
047    import javax.swing.JButton;
048    import javax.swing.JCheckBox;
049    import javax.swing.JLabel;
050    import javax.swing.JPanel;
051    import javax.swing.JRadioButton;
052    import javax.swing.JScrollPane;
053    
054    import org.bushe.swing.event.EventBus;
055    import org.bushe.swing.event.annotation.AnnotationProcessor;
056    import org.bushe.swing.event.annotation.EventTopicSubscriber;
057    import org.slf4j.Logger;
058    import org.slf4j.LoggerFactory;
059    
060    import ucar.unidata.idv.IdvObjectStore;
061    import ucar.unidata.ui.CheckboxCategoryPanel;
062    import ucar.unidata.util.GuiUtils;
063    import ucar.unidata.xml.PreferenceManager;
064    import ucar.unidata.xml.XmlObjectStore;
065    
066    import edu.wisc.ssec.mcidasv.Constants;
067    import edu.wisc.ssec.mcidasv.McIDASV;
068    import edu.wisc.ssec.mcidasv.McIdasPreferenceManager;
069    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
070    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
071    import edu.wisc.ssec.mcidasv.util.Contract;
072    
073    /**
074     * The ADDE Server preference panel is <b>almost</b> a read-only {@literal "view"}
075     * of the current state of the server manager. The only thing that users can 
076     * change from here is the visibility of the individual {@link AddeEntry}s, 
077     * though there has been some talk of allowing for reordering.
078     */
079    public class AddePreferences {
080    
081        public enum Selection { ALL_ENTRIES, SPECIFIED_ENTRIES };
082    
083        private static final Logger logger = LoggerFactory.getLogger(AddePreferences.class);
084    
085        /** 
086         * Property ID for controlling the display of {@literal "site"} servers 
087         * in the server preferences. 
088         */
089        private static final String PREF_LIST_SITE_SERV = "mcv.servers.listsite";
090    
091        /** 
092         * Property ID for controlling the display of {@literal "default mcv"} 
093         * servers in the server preferences. 
094         */
095        private static final String PREF_LIST_DEFAULT_SERV = "mcv.servers.listdefault";
096    
097        /** 
098         * Property ID for controlling the display of {@literal "MCTABLE"} 
099         * servers in the server preferences. 
100         */
101        private static final String PREF_LIST_MCTABLE_SERV = "mcv.servers.listmcx";
102    
103        /** 
104         * Property ID for controlling the display of {@literal "user"} servers 
105         * in the server preferences. 
106         */
107        private static final String PREF_LIST_USER_SERV = "mcv.servers.listuser";
108    
109        /**
110         * Property ID that allows McIDAS-V to remember whether or not the user
111         * has chosen to use all available ADDE servers or has specified the 
112         * {@literal "active"} servers.
113         */
114        private static final String PREF_LIST_SPECIFY = "mcv.servers.pref.specify";
115    
116        // TODO need to get open/close methods added to CheckboxCategoryPanel
117    //    private static final String PREF_LIST_TYPE_PREFIX = "mcv.servers.types.list";
118    
119        /** Contains the lists of ADDE servers that we'll use as content. */
120        private final EntryStore entryStore;
121    
122        /** Panel that contains the various {@link AddeEntry}s. */
123        private JPanel cbPanel = null;
124    
125        private JScrollPane cbScroller = null;
126    
127        /**
128         * Allows the user to enable all {@link AddeEntry}s, <b><i>without</i></b> 
129         * disabling the preference panel.
130         */
131        private JButton allOn = null;
132    
133        /**
134         * Allows the user to disable all {@link AddeEntry}s, <b><i>without</i></b> 
135         * disabling the preference panel.
136         */
137        private JButton allOff = null;
138    
139        /**
140         * Prepares a new preference panel based upon the supplied 
141         * {@link EntryStore}.
142         * 
143         * @param entryStore The {@code EntryStore} to query. Cannot be 
144         * {@code null}.
145         * 
146         * @throws NullPointerException if {@code entryStore} is {@code null}.
147         */
148        public AddePreferences(final EntryStore entryStore) {
149            AnnotationProcessor.process(this);
150            if (entryStore == null) {
151                throw new NullPointerException("EntryStore cannot be null");
152            }
153            this.entryStore = entryStore;
154        }
155    
156        /**
157         * Adds the various {@link AddePrefConglomeration} objects to the {@code prefManager}.
158         * 
159         * @param prefManager McIDAS-V's {@link PreferenceManager}. Should not be {@code null}.
160         */
161        public void addPanel(McIdasPreferenceManager prefManager) {
162            AddePrefConglomeration notPretty = buildPanel((McIDASV)prefManager.getIdv());
163            // add the panel, listeners, and so on to the preference manager.
164            prefManager.add(notPretty.getName(), "blah", notPretty.getEntryListener(), 
165                notPretty.getEntryPanel(), notPretty.getEntryToggles());
166        }
167    
168        /**
169         * Listens for {@link ucar.unidata.ui.CheckboxCategoryPanel} updates and
170         * stores the current status.
171         * 
172         * @param topic Topic of interest is {@code "CheckboxCategoryPanel.PanelToggled"}.
173         * @param catPanel The object that changed.
174         */
175        @EventTopicSubscriber(topic="CheckboxCategoryPanel.PanelToggled")
176        public void handleCategoryToggle(final String topic, final CheckboxCategoryPanel catPanel) {
177            IdvObjectStore store = entryStore.getIdvStore();
178            store.put("addepref.category."+catPanel.getCategoryName(), catPanel.isOpen());
179        }
180    
181        /**
182         * Builds the remote server preference panel, using the given 
183         * {@link McIdasPreferenceManager}.
184         * 
185         * @param mcv Reference to the McIDAS-V object; mostly used to control the 
186         * server manager GUI. Cannot be {@code null}.
187         * 
188         * @return An object containing the various components required of a 
189         * preference panel.
190         */
191        public AddePrefConglomeration buildPanel(final McIDASV mcv) {
192            Contract.notNull(mcv, "Cannot build a preference panel with a null McIDASV object");
193            Map<EntryType, Set<AddeEntry>> entries = 
194                entryStore.getVerifiedEntriesByTypes();
195    
196            final Map<AddeEntry, JCheckBox> entryToggles = 
197                new LinkedHashMap<AddeEntry, JCheckBox>();
198    
199            final List<CheckboxCategoryPanel> typePanels = arrList();
200            List<JPanel> compList = arrList();
201    
202            final IdvObjectStore store = mcv.getStore();
203    
204            // create checkboxes for each AddeEntry and add 'em to the appropriate 
205            // CheckboxCategoryPanel 
206            for (final EntryType type : EntryType.values()) {
207                if (EntryType.INVALID.equals(type)) {
208                    continue;
209                }
210                final Set<AddeEntry> subset = entries.get(type);
211                Set<String> observedEntries = new HashSet<String>(subset.size());
212                boolean typePanelVis = store.get("addepref.category."+type.toString(), false);
213                final CheckboxCategoryPanel typePanel = 
214                    new CheckboxCategoryPanel(type.toString(), typePanelVis);
215    
216                for (final AddeEntry entry : subset) {
217                    final String entryText = entry.getEntryText();
218                    if (observedEntries.contains(entryText)) {
219                        continue;
220                    }
221    
222                    boolean enabled = (entry.getEntryStatus() == EntryStatus.ENABLED);
223                    final JCheckBox cbx = new JCheckBox(entryText, enabled);
224                    cbx.addActionListener(new ActionListener() {
225                        public void actionPerformed(final ActionEvent e) {
226                            EntryStatus status = (cbx.isSelected()) ? EntryStatus.ENABLED : EntryStatus.DISABLED;
227                            logger.trace("entry={} val={} status={} evt={}", new Object[] { entryText, cbx.isSelected(), status, e});
228                            entry.setEntryStatus(status);
229                            entryToggles.put(entry, cbx);
230                            store.put("addeprefs.scroller.pos", cbScroller.getViewport().getViewPosition());
231                            EventBus.publish(EntryStore.Event.UPDATE);
232                        }
233                    });
234                    entryToggles.put(entry, cbx);
235                    typePanel.addItem(cbx);
236                    typePanel.add(GuiUtils.inset(cbx, new Insets(0, 20, 0, 0)));
237                    observedEntries.add(entryText);
238                }
239    
240                compList.add(typePanel.getTopPanel());
241                compList.add(typePanel);
242                typePanels.add(typePanel);
243                typePanel.checkVisCbx();
244            }
245    
246            // create the basic pref panel
247            // TODO(jon): determine dimensions more intelligently!
248            cbPanel = GuiUtils.top(GuiUtils.vbox(compList));
249            cbScroller = new JScrollPane(cbPanel);
250            cbScroller.getVerticalScrollBar().setUnitIncrement(10);
251            cbScroller.setPreferredSize(new Dimension(300, 300));
252            Point oldPos = (Point)store.get("addeprefs.scroller.pos");
253            if (oldPos == null) {
254                oldPos = new Point(0,0);
255            }
256            cbScroller.getViewport().setViewPosition(oldPos);
257    
258    
259            // handle the user opting to enable all servers
260            allOn = new JButton("All on");
261            allOn.addActionListener(new ActionListener() {
262                public void actionPerformed(final ActionEvent e) {
263                    for (CheckboxCategoryPanel cbcp : typePanels) {
264                        cbcp.toggleAll(true);
265                    }
266                }
267            });
268    
269            // handle the user opting to disable all servers
270            allOff = new JButton("All off");
271            allOff.addActionListener(new ActionListener() {
272                public void actionPerformed(final ActionEvent e) {
273                    for (CheckboxCategoryPanel cbcp : typePanels) {
274                        cbcp.toggleAll(false);
275                    }
276                }
277            });
278    
279            // user wants to add a server! make it so.
280            final JButton addServer = new JButton("Add ADDE Servers...");
281            addServer.addActionListener(new ActionListener() {
282                public void actionPerformed(final ActionEvent e) {
283                    mcv.showServerManager();
284                }
285            });
286    
287            // import list of servers
288            final JButton importServers = new JButton("Import Servers...");
289            importServers.addActionListener(new ActionListener() {
290                public void actionPerformed(final ActionEvent e) {
291                    mcv.showServerManager();
292                }
293            });
294    
295            boolean useAll = false;
296            boolean specify = false;
297            if (Selection.ALL_ENTRIES.equals(getSpecifyServers())) {
298                useAll = true;
299            } else {
300                specify = true;
301            }
302    
303            // disable user selection of entries--they're using everything
304            final JRadioButton useAllBtn = 
305                new JRadioButton("Use all ADDE entries", useAll);
306            useAllBtn.addActionListener(new ActionListener() {
307                public void actionPerformed(ActionEvent ae) {
308                    setGUIEnabled(!useAllBtn.isSelected());
309                    // TODO(jon): use the eventbus
310                    setSpecifyServers(Selection.ALL_ENTRIES);
311                    EventBus.publish(EntryStore.Event.UPDATE); // doesn't work...
312                }
313            });
314    
315            // let the user specify the "active" set, enable entry selection
316            final JRadioButton useTheseBtn = 
317                new JRadioButton("Use selected ADDE entries:", specify);
318            useTheseBtn.addActionListener(new ActionListener() {
319                public void actionPerformed(ActionEvent ae) {
320                    setGUIEnabled(!useAllBtn.isSelected());
321                    // TODO(jon): use the eventbus
322                    setSpecifyServers(Selection.SPECIFIED_ENTRIES);
323                    EventBus.publish(EntryStore.Event.UPDATE); // doesn't work...
324                }
325            });
326            GuiUtils.buttonGroup(useAllBtn, useTheseBtn);
327    
328            // force the selection state
329            setGUIEnabled(!useAllBtn.isSelected());
330    
331            JPanel widgetPanel =
332                GuiUtils.topCenter(
333                    GuiUtils.hbox(useAllBtn, useTheseBtn),
334                    GuiUtils.leftCenter(
335                        GuiUtils.inset(
336                            GuiUtils.top(GuiUtils.vbox(allOn, allOff, addServer, importServers)),
337                            4), cbScroller));
338    
339            JPanel entryPanel =
340                GuiUtils.topCenter(
341                    GuiUtils.inset(
342                        new JLabel("Specify the active ADDE servers:"),
343                        4), widgetPanel);
344            entryPanel = GuiUtils.inset(GuiUtils.left(entryPanel), 6);
345    
346            // iterate through all the entries and "apply" any changes: 
347            // reordering, removing, visibility changes, etc
348            PreferenceManager entryListener = new PreferenceManager() {
349                public void applyPreference(XmlObjectStore store, Object data) {
350    //                logger.trace("well, the damn thing fires at least");
351    //                // this won't break because the data parameter is whatever
352    //                // has been passed in to "prefManager.add(...)". in this case,
353    //                // it's the "entryToggles" variable.
354                    store.put("addeprefs.scroller.pos", cbScroller.getViewport().getViewPosition());
355                    
356                    @SuppressWarnings("unchecked")
357                    Map<AddeEntry, JCheckBox> toggles = (Map<AddeEntry, JCheckBox>)data;
358                    boolean updated = false;
359                    for (Entry<AddeEntry, JCheckBox> entry : toggles.entrySet()) {
360                        AddeEntry e = entry.getKey();
361                        JCheckBox c = entry.getValue();
362                        EntryStatus currentStatus = e.getEntryStatus();
363                        EntryStatus nextStatus = (c.isSelected()) ? EntryStatus.ENABLED : EntryStatus.DISABLED;
364                        logger.trace("entry={} type={} old={} new={}", new Object[] { e, e.getEntryType(), currentStatus, nextStatus });
365    //                    if (currentStatus != nextStatus) {
366                            e.setEntryStatus(nextStatus);
367                            toggles.put(e, c);
368                            updated = true;
369    //                    }
370                    }
371                    if (updated) {
372                        EventBus.publish(EntryStore.Event.UPDATE);
373                    }
374                }
375            };
376            return new AddePrefConglomeration(Constants.PREF_LIST_ADDE_SERVERS, entryListener, entryPanel, entryToggles);
377        }
378    
379        /**
380         * Enables or disables:<ul>
381         * <li>{@link JPanel} containing the {@link AddeEntry}s ({@link #cbPanel}).</li>
382         * <li>{@link JButton} that enables all available {@code AddeEntry}s ({@link #allOn}).</li>
383         * <li>{@code JButton} that disables all available {@code AddeEntry}s ({@link #allOff}).</li>
384         * </ul>
385         * Enabling the components allows the user to pick and choose servers, while
386         * disabling enables all servers.
387         * 
388         * @param enabled {@code true} enables the components and {@code false} disables.
389         */
390        public void setGUIEnabled(final boolean enabled) {
391            if (cbPanel != null) {
392                GuiUtils.enableTree(cbPanel, enabled);
393            }
394            if (allOn != null) {
395                GuiUtils.enableTree(allOn, enabled);
396            }
397            if (allOff != null) {
398                GuiUtils.enableTree(allOff, enabled);
399            }
400        }
401    
402        /**
403         * Sets the value of the {@link #PREF_LIST_SPECIFY} preference to 
404         * {@code value}. 
405         * 
406         * @param entrySelection New value to associate with {@code PREF_LIST_SPECIFY}.
407         */
408        private void setSpecifyServers(final Selection entrySelection) {
409            entryStore.getIdvStore().put(PREF_LIST_SPECIFY, entrySelection.toString());
410        }
411    
412        /**
413         * Returns the value of the {@link #PREF_LIST_SPECIFY} preference. Defaults
414         * to {@literal "ALL"}.
415         */
416        private Selection getSpecifyServers() {
417            String saved = entryStore.getIdvStore().get(PREF_LIST_SPECIFY, Selection.ALL_ENTRIES.toString());
418            Selection entrySelection;
419            if ("ALL".equalsIgnoreCase(saved)) {
420                entrySelection = Selection.ALL_ENTRIES;
421            } else if ("SPECIFY".equalsIgnoreCase(saved)) {
422                entrySelection = Selection.SPECIFIED_ENTRIES;
423            } else {
424                entrySelection = Selection.valueOf(saved);
425            }
426            
427            return entrySelection;
428        }
429    
430        /**
431         * This class is essentially a specialized tuple of the different things 
432         * required by the {@link ucar.unidata.idv.IdvPreferenceManager}.
433         */
434        public static class AddePrefConglomeration {
435            private final String name;
436            private final PreferenceManager entryListener;
437            private final JPanel entryPanel;
438            private final Map<AddeEntry, JCheckBox> entryToggles;
439            public AddePrefConglomeration(String name, PreferenceManager entryListener, JPanel entryPanel, Map<AddeEntry, JCheckBox> entryToggles) {
440                this.name = name;
441                this.entryListener = entryListener;
442                this.entryPanel = entryPanel;
443                this.entryToggles = entryToggles;
444            }
445            public String getName() { return name; }
446            public PreferenceManager getEntryListener() { return entryListener; }
447            public JPanel getEntryPanel() { return entryPanel; }
448            public Map<AddeEntry, JCheckBox> getEntryToggles() { return entryToggles; }
449        }
450    }