001/*
002 * $Id: AddePreferences.java,v 1.12 2011/03/24 16:06:34 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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 */
030package edu.wisc.ssec.mcidasv.servermanager;
031
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
033
034import java.awt.Dimension;
035import java.awt.Insets;
036import java.awt.Point;
037import java.awt.event.ActionEvent;
038import java.awt.event.ActionListener;
039import java.util.HashSet;
040import java.util.LinkedHashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044import java.util.Map.Entry;
045
046import javax.swing.Icon;
047import javax.swing.JButton;
048import javax.swing.JCheckBox;
049import javax.swing.JLabel;
050import javax.swing.JPanel;
051import javax.swing.JRadioButton;
052import javax.swing.JScrollPane;
053
054import org.bushe.swing.event.EventBus;
055import org.bushe.swing.event.annotation.AnnotationProcessor;
056import org.bushe.swing.event.annotation.EventTopicSubscriber;
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059
060import ucar.unidata.idv.IdvObjectStore;
061import ucar.unidata.ui.CheckboxCategoryPanel;
062import ucar.unidata.util.GuiUtils;
063import ucar.unidata.xml.PreferenceManager;
064import ucar.unidata.xml.XmlObjectStore;
065
066import edu.wisc.ssec.mcidasv.Constants;
067import edu.wisc.ssec.mcidasv.McIDASV;
068import edu.wisc.ssec.mcidasv.McIdasPreferenceManager;
069import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
070import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
071import 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 */
079public 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            }
377        };
378        return new AddePrefConglomeration(Constants.PREF_LIST_ADDE_SERVERS, entryListener, entryPanel, entryToggles);
379    }
380
381    /**
382     * Enables or disables:<ul>
383     * <li>{@link JPanel} containing the {@link AddeEntry}s ({@link #cbPanel}).</li>
384     * <li>{@link JButton} that enables all available {@code AddeEntry}s ({@link #allOn}).</li>
385     * <li>{@code JButton} that disables all available {@code AddeEntry}s ({@link #allOff}).</li>
386     * </ul>
387     * Enabling the components allows the user to pick and choose servers, while
388     * disabling enables all servers.
389     * 
390     * @param enabled {@code true} enables the components and {@code false} disables.
391     */
392    public void setGUIEnabled(final boolean enabled) {
393        if (cbPanel != null) {
394            GuiUtils.enableTree(cbPanel, enabled);
395        }
396        if (allOn != null) {
397            GuiUtils.enableTree(allOn, enabled);
398        }
399        if (allOff != null) {
400            GuiUtils.enableTree(allOff, enabled);
401        }
402    }
403
404    /**
405     * Sets the value of the {@link #PREF_LIST_SPECIFY} preference to 
406     * {@code value}. 
407     * 
408     * @param value New value to associate with {@code PREF_LIST_SPECIFY}.
409     */
410    private void setSpecifyServers(final Selection entrySelection) {
411        entryStore.getIdvStore().put(PREF_LIST_SPECIFY, entrySelection.toString());
412    }
413
414    /**
415     * Returns the value of the {@link #PREF_LIST_SPECIFY} preference. Defaults
416     * to {@literal "ALL"}.
417     */
418    private Selection getSpecifyServers() {
419        String saved = entryStore.getIdvStore().get(PREF_LIST_SPECIFY, Selection.ALL_ENTRIES.toString());
420        Selection entrySelection;
421        if ("ALL".equalsIgnoreCase(saved)) {
422            entrySelection = Selection.ALL_ENTRIES;
423        } else if ("SPECIFY".equalsIgnoreCase(saved)) {
424            entrySelection = Selection.SPECIFIED_ENTRIES;
425        } else {
426            entrySelection = Selection.valueOf(saved);
427        }
428        
429        return entrySelection;
430    }
431
432    /**
433     * This class is essentially a specialized tuple of the different things 
434     * required by the {@link ucar.unidata.idv.IdvPreferenceManager}.
435     */
436    public static class AddePrefConglomeration {
437        private final String name;
438        private final PreferenceManager entryListener;
439        private final JPanel entryPanel;
440        private final Map<AddeEntry, JCheckBox> entryToggles;
441        public AddePrefConglomeration(String name, PreferenceManager entryListener, JPanel entryPanel, Map<AddeEntry, JCheckBox> entryToggles) {
442            this.name = name;
443            this.entryListener = entryListener;
444            this.entryPanel = entryPanel;
445            this.entryToggles = entryToggles;
446        }
447        public String getName() { return name; }
448        public PreferenceManager getEntryListener() { return entryListener; }
449        public JPanel getEntryPanel() { return entryPanel; }
450        public Map<AddeEntry, JCheckBox> getEntryToggles() { return entryToggles; }
451    }
452}