001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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 https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv;
030
031import java.util.List;
032import java.util.Stack;
033
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import ucar.unidata.idv.IntegratedDataViewer;
038import ucar.unidata.idv.VMManager;
039import ucar.unidata.idv.ViewDescriptor;
040import ucar.unidata.idv.ViewManager;
041import ucar.unidata.idv.control.DisplayControlImpl;
042import ucar.unidata.ui.ComponentHolder;
043import ucar.unidata.util.GuiUtils;
044
045import edu.wisc.ssec.mcidasv.ui.McvComponentHolder;
046import edu.wisc.ssec.mcidasv.ui.UIManager;
047
048/**
049 * <p>McIDAS-V needs to manage ViewManagers in a slightly different way than 
050 * the IDV. The key differences between the two are the way previously active 
051 * ViewManagers are ordered and a (hopefully) more consistent way of handling 
052 * active ViewManagers.</p>
053 * 
054 * <p>The IDV only keeps track of the ViewManager used immediately before the 
055 * current one. McV keeps track of the previously active ViewManagers in a 
056 * stack. This mimics window z-ordering and always returns the user to the most
057 * recently active ViewManager upon removal of the active ViewManager.</p>
058 * 
059 * <p>Newly created ViewManagers and their first layer now become the active
060 * ViewManager and layer. If there is only one ViewManager, it is now displayed
061 * as active instead of just implying it. When the active ViewManager is 
062 * removed, the last active ViewManager and its first layer become active.</p>
063 * 
064 * <p><b>A note to the future</b>: McV/IDV supports two notions of active and 
065 * selected ViewManagers. Say you have NxN ViewManagers in a ComponentHolder, 
066 * and you want to share views among some of these ViewManagers. When one of 
067 * these shared ViewManagers is activated, should all of them become the active
068 * ViewManager? We're going to have to work out how to convey which 
069 * ViewManagers are shared and active, and maybe more? Good luck!</p>
070 */
071// TODO: should accesses to previousVMs should be synchronized?
072// TODO: should keep track of the ordering of active layers per VM as well.
073public class ViewManagerManager extends VMManager {
074
075    private static final Logger logger = LoggerFactory.getLogger(ViewManagerManager.class);
076
077    /** Whether or not to display debug messages. */
078    private final boolean DEBUG = false;
079
080    /** The stack that stores the order of previously active ViewManagers. */
081    private final Stack<ViewManager> previousVMs = new Stack<ViewManager>();
082
083    /** Convenient reference back to the UIManager. */
084    private UIManager uiManager;
085
086    /**
087     * Create a ... manager of {@link ViewManager ViewManagers}.
088     *
089     * @param idv Reference to the application session.
090     *            Cannot be {@code null}.
091     */
092    public ViewManagerManager(IntegratedDataViewer idv) {
093        super(idv);
094        uiManager = (UIManager)getIdvUIManager();
095    }
096
097    public int getViewManagerCount() {
098        return getViewManagers().size();
099    }
100
101    /**
102     * Add the new view manager into the list if we don't have
103     * one with the {@link ViewDescriptor} of the new view manager
104     * already.
105     *
106     * @param newViewManager The new view manager
107     */
108    @Override public void addViewManager(ViewManager newViewManager) {
109        super.addViewManager(newViewManager);
110        focusLayerControlsOn(newViewManager, false);
111    }
112
113    /**
114     * @return Reference to the stack of previously active ViewManagers.
115     */
116    public Stack<ViewManager> getViewManagerOrder() {
117        return previousVMs;
118    }
119
120    /**
121     * Overridden so that McV can set the active ViewManager even if there is
122     * only one ViewManager. This is just a UI nicety; it'll allow the McV UI
123     * to show the active ViewManager no matter what.
124     * 
125     * @return Always returns true.
126     */
127    @Override public boolean haveMoreThanOneMainViewManager() {
128        return true;
129    }
130
131    /**
132     * Handles the removal of a ViewManager. McV needs to override this so that
133     * the stack of previously active ViewManagers is ordered properly. McV uses
134     * this method to make the ViewPanel respond immediately to the change.
135     * 
136     * @param viewManager The ViewManager being removed.
137     */
138    @Override public void removeViewManager(ViewManager viewManager) {
139        // the ordering of the stack must be preserved! this is the only chance
140        // to ensure the ordering if the incoming VM is inactive.
141        if (getLastActiveViewManager() != viewManager) {
142            previousVMs.remove(viewManager);
143            inspectStack("removing inactive vm");
144        }
145
146        // now just sit back and let the IDV and setLastActiveViewManager work
147        // their magic.
148        super.removeViewManager(viewManager);
149
150        // inform UIManager that the VM needs to be dissociated from its
151        // ComponentHolder.
152        uiManager.removeViewManagerHolder(viewManager);
153
154        // force the layer controls tabs to layout the remaining components, 
155        // but we don't want to bring it to the front!
156        uiManager.getViewPanel().getContents().validate();
157    }
158
159    /**
160     * <p>This method is a bit strange. If the given ViewManager is null, then 
161     * the IDV has removed the active ViewManager. McV will use the stack of 
162     * last active ViewManagers to make the last active ViewManager active once 
163     * again.</p>
164     * 
165     * <p>If the given ViewManager is not null, but cannot be found in the stack
166     * of previously active ViewManagers, the IDV has created a new ViewManager 
167     * and McV must push it on the stack.</p>
168     * 
169     * <p>If the given ViewManager is not null and has been found in the stack,
170     * then the user has selected an inactive ViewManager. McV must remove the
171     * ViewManager from the stack and then push it back on top.</p>
172     * 
173     * <p>These steps allow McV to make the behavior of closing tabs a bit more
174     * user-friendly. The user is always returned to whichever ViewManager was
175     * last active.</p>
176     * 
177     * @param vm See above. :(
178     */
179    // TODO: when you start removing the debug stuff, just convert the messages
180    // to comments.
181    @Override public void setLastActiveViewManager(ViewManager vm) {
182        String debugMsg = "created new vm";
183        if (vm != null) {
184            if (previousVMs.search(vm) >= 0) {
185                debugMsg = "reset active vm";
186                previousVMs.remove(vm);
187                focusLayerControlsOn(vm, false);
188            }
189            previousVMs.push(vm);
190        } else {
191            debugMsg = "removed active vm";
192
193            ViewManager lastActive = getLastActiveViewManager();
194            if (lastActive == null)
195                return;
196
197            lastActive.setLastActive(false);
198
199            previousVMs.pop();
200
201            // if there are no more VMs, make sure the IDV code knows about it
202            // by setting the last active VM to null.
203            if (previousVMs.isEmpty()) {
204                super.setLastActiveViewManager(null);
205                return;
206            }
207
208            lastActive = previousVMs.peek();
209            lastActive.setLastActive(true);
210
211            focusLayerControlsOn(lastActive, false);
212        }
213
214        inspectStack(debugMsg);
215        super.setLastActiveViewManager(previousVMs.peek());
216
217        // start active tab testing
218        ComponentHolder holder = 
219            uiManager.getViewManagerHolder(previousVMs.peek());
220        if ((holder != null) && (holder instanceof McvComponentHolder)) {
221            ((McvComponentHolder)holder).setAsActiveTab();
222        }
223        // stop active tab testing
224    }
225
226    /**
227     * <p>Overwrite the stack containing the ordering of previously active 
228     * ViewManagers.</p>
229     * 
230     * <p>Use this if you want to mess with the user's mind a little bit.</p>
231     * 
232     * @param newOrder The stack containing the new ordering of ViewManagers.
233     */
234    public void setViewManagerOrder(Stack<ViewManager> newOrder) {
235        previousVMs.clear();
236        previousVMs.addAll(newOrder);
237    }
238
239    public int getComponentHolderCount() {
240        return -1;
241    }
242
243    public int getComponentGroupCount() {
244        // should be the same as the number of windows (or perhaps numWindows-1).
245        return -1;
246    }
247
248    /**
249     * Sets the active tab of the dashboard to the layer controls and makes the
250     * first layer (TODO: fix that!) of the given ViewManager the active layer.
251     * 
252     * @param vm The ViewManager to make active.
253     * @param doShow Whether or not the layer controls should become the active
254     *               tab in the dashboard.
255     */
256    private void focusLayerControlsOn(ViewManager vm, boolean doShow) {
257        List<DisplayControlImpl> controls = vm.getControlsForLegend();
258        if (controls != null && !controls.isEmpty()) {
259            DisplayControlImpl control = controls.get(0);
260            if (doShow) {
261                GuiUtils.showComponentInTabs(control.getOuterContents(), false);
262            }
263        }
264    }
265
266    /**
267     * Helper method that'll display the ordering of the stack and a helpful
268     * debug message!
269     *
270     * @param msg Message to include in output.
271     */
272    private void inspectStack(String msg) {
273        if (!DEBUG) {
274            return;
275        }
276        StringBuilder sb = new StringBuilder(this.hashCode()).append(": ").append(msg).append(": [");
277        for (ViewManager vm : previousVMs) {
278            sb.append(vm.hashCode()).append(',');
279        }
280        logger.trace(sb.append("] Size=").append(previousVMs.size()).toString());
281    }
282
283    /**
284     * Turns off layer visibility animation for all {@code ViewManager}s. This
285     * is typically only useful for when the user has removed all layers 
286     * <i>without</i> turning off the layer animation setting.
287     */
288    protected void disableAllLayerVizAnimations() {
289        for (ViewManager vm : getViewManagers()) {
290            vm.setAnimatedVisibilityCheckBox(false);
291        }
292    }
293}