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