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