/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import tufts.Util; import tufts.vue.gui.DnDTabbedPane; import tufts.vue.gui.GUI; import java.awt.*; import java.awt.event.*; import javax.swing.JTabbedPane; import javax.swing.JScrollPane; import java.util.Iterator; import java.util.ArrayList; /** * Code for handling a tabbed pane of MapViewer's: adding, removing, * keeping tab labels current & custom appearance tweaks. * * @version $Revision: 1.54 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $ */ // todo: need to figure out how to have the active map grab // the focus if no other map has focus: switching tabs // changes the map you're looking it, and it's set to // the active map, but it doesn't get focus unless you click on it! public class MapTabbedPane extends JTabbedPane//extends DnDTabbedPane implements LWComponent.Listener, FocusListener, MapViewer.Listener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MapTabbedPane.class); private final String name; private final Color BgColor; private int overTabIndex = -1; //private CloseTabPaneUI paneUI; // //private final boolean isLeftViewer; // private static MapTabbedPane leftTabs; // private static MapTabbedPane rightTabs; MapTabbedPane(String name, boolean isLeft) { this.name = name; setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); // //this.isLeftViewer = isLeft; // if (isLeft) // leftTabs = this; // else // rightTabs = this; setName("mapTabs-" + name); setFocusable(false); BgColor = GUI.getToolbarColor(); setTabPlacement(javax.swing.SwingConstants.TOP); //if (DEBUG.Enabled) //setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT); // appears to have no effect in Aqua on Mac setPreferredSize(new Dimension(300,400)); //VUE.addActiveListener(LWMap.class, this); EventHandler.addListener(MapViewer.Event.class, this); // super.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); //paneUI = new CloseTabPaneUI(); //super.setUI(paneUI); /*//getModel(). addChangeListener(new javax.swing.event.ChangeListener() { public void stateChanged(javax.swing.event.ChangeEvent e) { if (DEBUG.Enabled) out("stateChanged: selectedIndex=" + getSelectedIndex()); } });*/ } // public void activeChanged(ActiveEvent e, LWMap map) { // if (!isLeftViewer || e.hasSourceOfType(MapTabbedPane.class)) { // // ignore change events from the other tab-pane // return; // } // int mapIndex = findTabWithMap(map); // if (mapIndex >= 0) // setSelectedIndex(mapIndex); // } private int mWasSelected = -1; // non-aqua use only @Override protected void fireStateChanged() { try { if (DEBUG.FOCUS) out("fireStateChanged, selectedIndex=" +getSelectedIndex() + "; viewerAtIndex=" + getViewerAt(getSelectedIndex()));; super.fireStateChanged(); } catch (ArrayIndexOutOfBoundsException e) { // this is happening after we close everything and then // open another map -- no idea why, but this successfully // ignores it. System.err.println(this + " JTabbedPane.fireStateChanged: " + e); } if (GUI.isMacAqua() == false) { int selected = getModel().getSelectedIndex(); // for non-aqua UI's, we change the selected tab color if (mWasSelected >= 0) setForegroundAt(mWasSelected, Color.darkGray); if (selected >= 0) { setForegroundAt(selected, Color.black); setBackgroundAt(selected, BgColor); } mWasSelected = selected; } } @Override public void setSelectedIndex(final int index) { // if (index < 0) { // Log.debug("invalid tab index: " + index); // return; // } super.setSelectedIndex(index); final MapViewer viewer = getViewerAt(index); if (viewer != null /*&& !VUE.isStartupUnderway()*/) { if (DEBUG.FOCUS) out("ATTEMPTING FOCUS TRANSFER TO " + viewer); viewer.setFastPaint("tabbedTo"); // For some reason, that I think has to do with having lots of maps open and // using the drop-down menu for selected an open map (on the mac), focus // diagnostics show what is presumably the menu going hidden (a pop-up // heavy-weight window anyway) after a menu item is selected, and then after // this, the OLD viewer in the de-activating tab-pane first gets the focus // back, even if we have the newly selected viewer request focus here. By // putting the focus request at the end of the AWT event queue, all this // crap can happen, and then we can change the focus like we want. GUI.invokeAfterAWT(new Runnable() { public void run() { if (DEBUG.FOCUS) out("after AWT focus jibber-jabber (now at end of AWT event queue), focus is being demanded by: " + viewer); //viewer.requestFocus(); // do NOT use this, as the viewers may be temporaily, deliberately, unfocusable while we re-route focus // this can work (viewer can listen for itself going active and grab), but // we'd need to disable focusability on the viewer in the non active tab // pane first, as it's still grabbing focus back. //VUE.setActive(MapViewer.class, this, viewer); // We must call this directly as it will ensure focusability has been restored // on the viewer, as new viewers have this turned off by default initially, // so we can direct focus as desired (even non-visible right-viewers will // delightfully grab the focus when created if you let them). viewer.grabVueApplicationFocus(toString() + ".setSelectedIndex="+index + " " + viewer, null); }}); } } public void reshape(int x, int y, int w, int h) { boolean ignore = getX() == x && getY() == y && getWidth() == w && getHeight() == h; // if w or h <= 0 we can know we're being hidden //System.out.println(this + " reshape " + x + "," + y + " " + w + "x" + h + (ignore?" (IGNORING)":"")); super.reshape(x,y, w,h); } public MapViewer getSelectedViewer() { return getViewerAt(getSelectedIndex()); } public void advanceToNextTab() { if (getSelectedIndex() >= getTabCount() - 1) setSelectedIndex(0); else setSelectedIndex(getSelectedIndex() + 1); } public void focusGained(FocusEvent e) { out("focusGained (from " + e.getOppositeComponent() + ")"); } public void focusLost(FocusEvent e) { out("focusLost (to " + e.getOppositeComponent() + ")"); } public void addNotify() { super.addNotify(); if (!VueUtil.isMacPlatform()) { setForeground(Color.darkGray); setBackground(BgColor); } //addFocusListener(this); // hope not to hear anything... // don't let us be focusable or sometimes you can select // & activate a new map for interaction, but we keep // the focus here in the tabbed instead of giving to // the component in the tab. //setFocusable(false); // in constructor } private String mapToTabTitle(LWMap map) { String title = map.getLabel(); if (title.toLowerCase().endsWith(".vue") && title.length() > 4) title = title.substring(0, title.length() - 4); if (map.isCurrentlyFiltered()) title += "*"; return title; } private String viewerToTabTitle(MapViewer viewer) { if (DEBUG.WORK) return mapToTabTitle(viewer.getMap()); else return mapToTabTitle(viewer.getMap()) + " (" + ZoomTool.prettyZoomPercent(viewer.getZoomFactor()) + ")"; /* String title = mapToTabTitle(viewer.getMap()); // Present the zoom factor as a percentange title += " ("; double zoomPct = viewer.getZoomFactor() * 100; if (zoomPct < 10) { // if < 10% zoom, show with 1 digit of decimal value if it would be non-zero title += VueUtil.oneDigitDecimal(zoomPct); } else { //title += (int) Math.round(zoomPct); title += (int) Math.floor(zoomPct + 0.49); } return title + "%)"; */ } private void updateTitleTextAt(int i) { if (i >= 0) { MapViewer viewer = getViewerAt(i); setTitleAt(i, viewerToTabTitle(viewer)); LWMap map = viewer.getMap(); String tooltip = null; if (map.getFile() != null) tooltip = map.getFile().toString(); else tooltip = "<html> <i>Unsaved</i>  "; if (map.isCurrentlyFiltered()) tooltip += " (filtered)"; setToolTipTextAt(i, tooltip); } } static final int TitleChangeMask = MapViewer.Event.DISPLAYED | MapViewer.Event.FOCUSED | MapViewer.Event.ZOOM; // title includes zoom private static MapTabbedPane lastFocusPane; private static int lastFocusIndex = -1; public void eventRaised(MapViewer.Event e) { if ((e.id & TitleChangeMask) != 0) { int i = indexOfComponent(e.viewer); if (i >= 0) { updateTitleTextAt(i); if (e.id == MapViewer.Event.FOCUSED) { if (lastFocusPane != null) { //lastFocusPane.setIconAt(lastFocusIndex, null); lastFocusPane = null; } if (VUE.multipleMapsVisible()) { lastFocusPane = this; lastFocusIndex = i; //setIconAt(i, new BlobIcon(5,5, Color.green)); } } } } } public void LWCChanged(LWCEvent e) { if (e.getSource() instanceof LWMap) updateTitleTextAt(findTabWithMap((LWMap)e.getSource())); } public void addViewer(MapViewer viewer) { Component c = new tufts.vue.gui.MapScrollPane(viewer); if (false) { String tabTitle = viewerToTabTitle(viewer); if (tabTitle == null) tabTitle = "unknown"; System.out.println("Adding tab '" + tabTitle + "' component=" + c); addTab(tabTitle, c); } else { addTab(viewerToTabTitle(viewer), c); } LWMap map = viewer.getMap(); map.addLWCListener(this, LWKey.MapFilter, LWKey.Label); // todo perf: we should be able to ask to listen only // for events from this object directly (that we don't // care to hear from it's children) updateTitleTextAt(indexOfComponent(c)); // first time just needed for tooltip } /* // put BACKINGSTORE mode on a diag switch and test performance difference -- the // obvious difference is vastly better performance if an inspector window is // obscuring any part of the canvas (or any other window for that mater), which // kills off a huge chunk of BLIT_SCROLL_MODE's optimization. However, using // backing store completely fucks up if we start hand-panning the map, tho I'm // presuming that's because the hand panning isn't being done thru the viewport yet. //sp.getViewport().setScrollMode(javax.swing.JViewport.BACKINGSTORE_SCROLL_MODE); public void addTab(LWMap pMap, Component c) { //scroller.getViewport().setScrollMode(javax.swing.JViewport.BACKINGSTORE_SCROLL_MODE); //super.addTab(pMap.getLabel(), c instanceof JScrollPane ? c : new JScrollPane(c)); super.addTab(pMap.getLabel(), c); pMap.addLWCListener(this); if (pMap.getFile() != null) setToolTipTextAt(indexOfComponent(c), pMap.getFile().toString()); } */ /** * Will find either the component index (default superclass * behavior), or, if the component found at any location * is a JScrollPane, look within it at the JViewport's * view, and if it matches the component sought, return that index. */ public int indexOfComponent(Component component) { for (int i = 0; i < getTabCount(); i++) { Component c = getComponentAt(i); if ((c != null && c.equals(component)) || (c == null && c == component)) { return i; } if (c instanceof JScrollPane) { if (component == ((JScrollPane)c).getViewport().getView()) return i; } } return -1; } public MapViewer getViewerAt(int index) { Object c; try { c = getComponentAt(index); } catch (ArrayIndexOutOfBoundsException e) { return null; } MapViewer viewer = null; if (c instanceof MapViewer) viewer = (MapViewer) c; else if (c instanceof JScrollPane) viewer = (MapViewer) ((JScrollPane)c).getViewport().getView(); return viewer; } public LWMap getMapAt(int index) { MapViewer viewer = getViewerAt(index); LWMap map = null; // if (viewer == null && VUE.inFullScreen()) // hack, but works for now: todo: cleaner // return VUE.getActiveMap(); if (viewer != null) map = viewer.getMap(); //System.out.println(this + " map at index " + index + " is " + map); return map; } public Iterator<LWMap> getAllMaps() { return getAllMapsBag().iterator(); } public java.util.Collection<LWMap> getAllMapsBag() { int tabs = getTabCount(); ArrayList<LWMap> list = new ArrayList(); for(int i= 0;i< tabs;i++){ LWMap m = getMapAt(i); list.add(m); } return list; } public MapViewer getViewerWithMap(LWMap map) { return getViewerAt(findTabWithMap(map)); } public void setSelectedMap(LWMap map) { setSelectedIndex(findTabWithMap(map)); } private int findTabWithMap(LWMap map) { int tabs = getTabCount(); for (int i = 0; i < tabs; i++) { LWMap m = getMapAt(i); if (m != null && m == map) { //System.out.println(this + " found map " + map + " at index " + i); return i; } } Log.error(this + ": failed to find map " + map + " at any index"); return -1; } public void closeMap(LWMap map) { if (DEBUG.FOCUS) out("closeMap " + map); int mapTabIndex = findTabWithMap(map); MapViewer viewer = getViewerAt(mapTabIndex); if (DEBUG.FOCUS) out("closeMap" + "\n\t indexOfMap=" + mapTabIndex + "\n\tviewerAtIndex=" + viewer + "\n\t activeViewer=" + VUE.getActiveViewer()); // Note: if we close out the last tab (while it's selected), the selected index // must change, and the JTabbedPane acts sanely, delivers change events, and // causes the MapViewer in the previously second-to-last-tab, now in the last // tab, to gain focus. However, if any OTHER tab is removed, technically the // selected index can (and does) stay the same, which makes sense, except the // selected ITEM is now different, and JTabbedPane is completely ignorant of // this, and does nothing, and delivers focus to nothing, so we must handle that // manually. This also reveals a weaknes the DefaultSingleSelectionModel, which // has no code to deal with the case of a selected item from change out from // under the selected index. So what we need to do is make sure the right // MapViewer forcably grabs the focus. /** * for more info on why the windows exception was added to to the below statement * see https://vue-forums.uit.tufts.edu/posts/list/484.pages */ boolean forceFocusTransfer = Util.isWindowsPlatform() ? true : false; if (viewer == VUE.getActiveViewer()) { // If this is the active viewer, we may need to manage // a focus transfer. // Apparently, even sometimes when it's the last tab that changes, JTabbedPane fails // to tansfer focus, so we do this always... //if (mapTabIndex != getTabCount() - 1) forceFocusTransfer = true; // Immediately make sure nothing can refer this this viewer. //VUE.setActive(MapViewer.class, this, null); // we might want to force notification even if selection is already empty: // we want all listeners, particularly the actions, to // update in case this is last map open VUE.getSelection().clear(); } removeTabAt(mapTabIndex); if (forceFocusTransfer) { int selectedIndex = getSelectedIndex(); // the newly selected tab doesn't always get the focus: if (DEBUG.FOCUS) out("closeMap force focus transfer to new selected tab index: " + selectedIndex); if (selectedIndex >= 0) getViewerAt(selectedIndex).grabVueApplicationFocus("closeMap", null); else VUE.setActive(MapViewer.class, this, null); // no open viewers } } public void paintComponent(Graphics g) { ((Graphics2D)g).setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON); super.paintComponent(g); } public String toString() { return "MapTabbedPane<"+name+">"; } private void out(String s) { System.out.println(this + ": " + s); } }