001/*
002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games.
003 * Copyright (C) 2000-2011 The Gridarta Developers.
004 *
005 * This program is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 2 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License along
016 * with this program; if not, write to the Free Software Foundation, Inc.,
017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
018 */
019
020package net.sf.gridarta.gui.mapdesktop;
021
022import java.beans.PropertyVetoException;
023import java.util.IdentityHashMap;
024import java.util.Iterator;
025import java.util.Map;
026import javax.swing.Action;
027import javax.swing.Icon;
028import javax.swing.ImageIcon;
029import javax.swing.JDesktopPane;
030import javax.swing.JInternalFrame;
031import javax.swing.JMenu;
032import javax.swing.event.InternalFrameEvent;
033import javax.swing.event.InternalFrameListener;
034import net.sf.gridarta.gui.map.mapview.MapView;
035import net.sf.gridarta.gui.map.mapview.MapViewManager;
036import net.sf.gridarta.gui.map.mapview.MapViewsListener;
037import net.sf.gridarta.gui.map.mapview.MapViewsManager;
038import net.sf.gridarta.gui.mapimagecache.MapImageCache;
039import net.sf.gridarta.gui.mapimagecache.MapImageCacheListener;
040import net.sf.gridarta.model.archetype.Archetype;
041import net.sf.gridarta.model.gameobject.GameObject;
042import net.sf.gridarta.model.maparchobject.MapArchObject;
043import net.sf.gridarta.model.mapcontrol.MapControl;
044import net.sf.gridarta.model.mapmanager.MapManager;
045import net.sf.gridarta.model.mapmanager.MapManagerListener;
046import net.sf.gridarta.utils.EditorAction;
047import net.sf.japi.swing.action.ActionBuilder;
048import net.sf.japi.swing.action.ActionBuilderFactory;
049import net.sf.japi.swing.action.ActionMethod;
050import org.apache.log4j.Category;
051import org.apache.log4j.Logger;
052import org.jetbrains.annotations.NotNull;
053import org.jetbrains.annotations.Nullable;
054
055/**
056 * The {@link JDesktopPane} containing all map views.
057 * @author <a href="mailto:cher@riedquat.de">Christian Hujer</a>
058 * @author Andreas Kirschbaum
059 */
060public class MapDesktop<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> extends JDesktopPane implements EditorAction {
061
062    /**
063     * The Logger for printing log messages.
064     */
065    @NotNull
066    private static final Category log = Logger.getLogger(MapDesktop.class);
067
068    /**
069     * The action builder.
070     */
071    @NotNull
072    private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta");
073
074    /**
075     * The serial version UID.
076     */
077    private static final long serialVersionUID = 1L;
078
079    /**
080     * All open map views.
081     */
082    @NotNull
083    private final MapViewManager<G, A, R> mapViewManager;
084
085    /**
086     * The {@link MapManager} to use.
087     */
088    @NotNull
089    private final MapManager<G, A, R> mapManager;
090
091    /**
092     * The {@link MapImageCache} to use.
093     */
094    @NotNull
095    private final MapImageCache<G, A, R> mapImageCache;
096
097    /**
098     * The {@link MapViewsManager}.
099     */
100    @NotNull
101    private final MapViewsManager<G, A, R> mapViewsManager;
102
103    /**
104     * The action for "prev window".
105     */
106    @Nullable
107    private Action aPrevWindow;
108
109    /**
110     * The action for "next window".
111     */
112    @Nullable
113    private Action aNextWindow;
114
115    /**
116     * The actions to select windows.
117     */
118    @NotNull
119    private final Map<MapView<G, A, R>, WindowAction<G, A, R>> windowActions = new IdentityHashMap<MapView<G, A, R>, WindowAction<G, A, R>>();
120
121    /**
122     * The {@link MapDesktop.MapViewFrameListener MapViewFrameListeners}
123     * associated with {@link MapView MapViews}. Maps map view to listener.
124     * @noinspection UnnecessarilyQualifiedInnerClassAccess
125     */
126    @NotNull
127    private final Map<MapView<G, A, R>, MapViewFrameListener> mapViewFrameListeners = new IdentityHashMap<MapView<G, A, R>, MapViewFrameListener>();
128
129    /**
130     * The {@link MapManagerListener} attached to {@link #mapManager}.
131     */
132    @NotNull
133    private final MapManagerListener<G, A, R> mapManagerListener = new MapManagerListener<G, A, R>() {
134
135        @Override
136        public void currentMapChanged(@Nullable final MapControl<G, A, R> mapControl) {
137            // ignore
138        }
139
140        @Override
141        public void mapCreated(@NotNull final MapControl<G, A, R> mapControl, final boolean interactive) {
142            mapViewsManager.addMapViewsListener(mapControl, mapViewsListener);
143        }
144
145        @Override
146        public void mapClosing(@NotNull final MapControl<G, A, R> mapControl) {
147            // ignore
148        }
149
150        @Override
151        public void mapClosed(@NotNull final MapControl<G, A, R> mapControl) {
152            mapViewsManager.removeMapViewsListener(mapControl, mapViewsListener);
153        }
154
155    };
156
157    /**
158     * The {@link MapViewsListener} attached to all existing {@link MapControl
159     * MapControls} for maps.
160     */
161    @NotNull
162    private final MapViewsListener<G, A, R> mapViewsListener = new MapViewsListener<G, A, R>() {
163
164        @Override
165        public void mapViewCreated(@NotNull final MapView<G, A, R> mapView) {
166            addMapView(mapView);
167        }
168
169        @Override
170        public void mapViewClosing(@NotNull final MapView<G, A, R> mapView) {
171            removeMapView(mapView);
172        }
173
174    };
175
176    /**
177     * The {@link MapImageCacheListener} registered to {@link #mapImageCache}.
178     */
179    @NotNull
180    private final MapImageCacheListener<G, A, R> mapImageCacheListener = new MapImageCacheListener<G, A, R>() {
181
182        @Override
183        public void iconChanged(@NotNull final MapControl<G, A, R> mapControl) {
184            final Iterator<MapView<G, A, R>> it = mapViewsManager.getMapViewIterator(mapControl);
185            while (it.hasNext()) {
186                updateFrameIcon(it.next());
187            }
188        }
189
190    };
191
192    /**
193     * Creates a new instance.
194     * @param mapViewManager all open map views
195     * @param mapManager the map manager to use
196     * @param mapImageCache the map image cache to use
197     * @param mapViewsManager the map views
198     */
199    public MapDesktop(@NotNull final MapViewManager<G, A, R> mapViewManager, @NotNull final MapManager<G, A, R> mapManager, @NotNull final MapImageCache<G, A, R> mapImageCache, @NotNull final MapViewsManager<G, A, R> mapViewsManager) {
200        this.mapViewManager = mapViewManager;
201        this.mapManager = mapManager;
202        this.mapImageCache = mapImageCache;
203        this.mapViewsManager = mapViewsManager;
204        mapManager.addMapManagerListener(mapManagerListener);
205        mapImageCache.addMapImageCacheListener(mapImageCacheListener);
206        updateFocus(false);
207    }
208
209    /**
210     * Sets the given level view as the current one.
211     * @param mapView the new current level view
212     */
213    public void setCurrentMapView(@NotNull final MapView<G, A, R> mapView) {
214        mapViewManager.setActiveMapView(mapView);
215        // De-iconify if necessary
216        final JInternalFrame internalFrame = mapView.getInternalFrame();
217        if (internalFrame.isIcon()) {
218            try {
219                internalFrame.setIcon(false);
220            } catch (final PropertyVetoException e) {
221                log.warn(ACTION_BUILDER.format("logUnexpectedException", e));
222            }
223            mapView.activate();
224            return;
225        }
226        updateFocus(true);
227        internalFrame.requestFocus();
228        internalFrame.restoreSubcomponentFocus();
229    }
230
231    /**
232     * Removes (closes) the map view.
233     * @param mapView the map view to be removed (closed)
234     */
235    private void removeMapView(@NotNull final MapView<G, A, R> mapView) {
236        final JInternalFrame internalFrame = mapView.getInternalFrame();
237        internalFrame.removeInternalFrameListener(mapViewFrameListeners.remove(mapView));
238        mapViewManager.removeMapView(mapView);
239        if (windowActions.remove(mapView) == null) {
240            assert false;
241        }
242        remove(internalFrame);
243        // This is important: Removing a JInternalFrame from a JDesktopPane doesn't deselect it.
244        // Thus it will still be referenced. To prevent a closed map from being referenced by Swing,
245        // we check whether it's selected and if so deselect it.
246        if (getSelectedFrame() == mapView) {
247            setSelectedFrame(null);
248        }
249        internalFrame.dispose();
250        repaint();
251
252        updateFocus(true);
253        refreshMenus();
254    }
255
256    /**
257     * Adds the map view.
258     * @param mapView the map view to add
259     */
260    private void addMapView(@NotNull final MapView<G, A, R> mapView) {
261        final WindowAction<G, A, R> windowAction = new WindowAction<G, A, R>(this, mapView, mapManager);
262        windowActions.put(mapView, windowAction);
263        updateFrameIcon(mapView);
264
265        final JInternalFrame internalFrame = mapView.getInternalFrame();
266        final MapViewFrameListener mapViewFrameListener = new MapViewFrameListener(mapView);
267        if (mapViewFrameListeners.put(mapView, mapViewFrameListener) != null) {
268            assert false;
269        }
270        internalFrame.addInternalFrameListener(mapViewFrameListener);
271        add(internalFrame);
272        mapViewManager.addMapView(mapView);
273        setCurrentMapView(mapView);
274        internalFrame.setVisible(true);
275        internalFrame.setBounds(0, 0, getWidth(), getHeight());
276        try {
277            internalFrame.setMaximum(true);
278        } catch (final PropertyVetoException e) {
279            log.error("PropertyVetoException: " + e);
280        }
281        refreshMenus();
282    }
283
284    /**
285     * Updates the frame icon to the current icon image.
286     * @param mapView the map view to update
287     */
288    private void updateFrameIcon(@NotNull final MapView<G, A, R> mapView) {
289        final Action windowAction = windowActions.get(mapView);
290        assert windowAction != null;
291        final Icon icon = new ImageIcon(mapImageCache.getOrCreateIcon(mapView.getMapControl()));
292        mapView.getInternalFrame().setFrameIcon(icon);
293        windowAction.putValue(Action.SMALL_ICON, icon);
294    }
295
296    /**
297     * Adds an action for selecting this window to a menu.
298     * @param menu the menu to add the action to
299     * @param mapView the map view to add
300     * @param index the index of the menu entry
301     */
302    public void addWindowAction(@NotNull final JMenu menu, @NotNull final MapView<G, A, R> mapView, final int index) {
303        final WindowAction<G, A, R> windowAction = windowActions.get(mapView);
304        assert windowAction != null;
305        windowAction.setIndex(index);
306        menu.add(windowAction);
307    }
308
309    /**
310     * Activates and raises the given map view.
311     * @param mapView the map view
312     */
313    private void activateAndRaiseMapView(@NotNull final MapView<G, A, R> mapView) {
314        mapManager.setCurrentMap(mapView.getMapControl());
315        mapView.activate();
316        final JInternalFrame internalFrame = mapView.getInternalFrame();
317        internalFrame.moveToFront();
318        setSelectedFrame(internalFrame);
319    }
320
321    /**
322     * Notifies that the map views focus is lost it is inserted as the second in
323     * line to the map view vector.
324     * @param mapView the map view who lost the focus
325     */
326    private void mapViewFocusLostNotify(@NotNull final MapView<G, A, R> mapView) {
327        mapViewManager.deactivateMapView(mapView);
328        updateFocus(true);
329    }
330
331    /**
332     * Notifies that the given map view is now set as the current one.
333     * @param mapView the new current map view
334     */
335    private void mapViewFocusGainedNotify(@NotNull final MapView<G, A, R> mapView) {
336        mapViewManager.activateMapView(mapView);
337        mapViewsManager.setFocus(mapView);
338        mapManager.setCurrentMap(mapView.getMapControl());
339    }
340
341    /**
342     * Updates the focus to the first non-iconified map window.
343     * @param careAboutIconification <code>true</code> if the focus update
344     * should ignore all windows iconified by the user.
345     */
346    private void updateFocus(final boolean careAboutIconification) {
347        // Show the next map (if such exists)
348        for (final MapView<G, A, R> mapView : mapViewManager) {
349            final JInternalFrame internalFrame = mapView.getInternalFrame();
350            if (internalFrame.isIcon()) {
351                if (!careAboutIconification) {
352                    try {
353                        internalFrame.setIcon(false);
354                    } catch (final PropertyVetoException e) {
355                        log.warn(ACTION_BUILDER.format("logUnexpectedException", e));
356                    }
357                    activateAndRaiseMapView(mapView);
358                    return;
359                }
360            } else {
361                activateAndRaiseMapView(mapView);
362                return;
363            }
364        }
365
366        // No non-iconified map windows found
367        mapManager.setCurrentMap(null);
368    }
369
370    /**
371     * Gives focus to the next window.
372     */
373    @ActionMethod
374    public void prevWindow() {
375        doPrevWindow(true);
376    }
377
378    /**
379     * Gives focus to the previous window.
380     */
381    @ActionMethod
382    public void nextWindow() {
383        doNextWindow(true);
384    }
385
386    /**
387     * Enables/disables the actions according to the current state.
388     */
389    private void refreshMenus() {
390        if (aPrevWindow != null) {
391            //noinspection ConstantConditions
392            aPrevWindow.setEnabled(doPrevWindow(false));
393        }
394        if (aNextWindow != null) {
395            //noinspection ConstantConditions
396            aNextWindow.setEnabled(doNextWindow(false));
397        }
398    }
399
400    /**
401     * Performs or checks availability of the "prev window" action.
402     * @param performAction whether the action should be performed
403     * @return whether the action was or can be performed
404     */
405    private boolean doPrevWindow(final boolean performAction) {
406        if (!mapViewManager.doPrevWindow(performAction)) {
407            return false;
408        }
409
410        if (performAction) {
411            updateFocus(false);
412        }
413
414        return true;
415    }
416
417    /**
418     * Performs or checks availability of the "next window" action.
419     * @param performAction whether the action should be performed
420     * @return whether the action was or can be performed
421     */
422    private boolean doNextWindow(final boolean performAction) {
423        if (!mapViewManager.doNextWindow(performAction)) {
424            return false;
425        }
426
427        if (performAction) {
428            updateFocus(false);
429        }
430
431        return true;
432    }
433
434    /**
435     * {@inheritDoc}
436     */
437    @Override
438    public void setAction(@NotNull final Action action, @NotNull final String name) {
439        if (name.equals("prevWindow")) {
440            aPrevWindow = action;
441        } else if (name.equals("nextWindow")) {
442            aNextWindow = action;
443        } else {
444            throw new IllegalArgumentException();
445        }
446        refreshMenus();
447    }
448
449    /**
450     * The listener attached to map views.
451     */
452    private class MapViewFrameListener implements InternalFrameListener {
453
454        /**
455         * The associated {@link MapView}.
456         */
457        @NotNull
458        private final MapView<G, A, R> mapView;
459
460        /**
461         * Creates a new instance.
462         * @param mapView the associated map view
463         */
464        private MapViewFrameListener(@NotNull final MapView<G, A, R> mapView) {
465            this.mapView = mapView;
466        }
467
468        @Override
469        public void internalFrameActivated(@NotNull final InternalFrameEvent e) {
470            mapViewFocusGainedNotify(mapView);
471        }
472
473        @Override
474        public void internalFrameClosed(@NotNull final InternalFrameEvent e) {
475            // ignore
476        }
477
478        @Override
479        public void internalFrameClosing(@NotNull final InternalFrameEvent e) {
480            mapViewsManager.closeMapView(mapView);
481        }
482
483        @Override
484        public void internalFrameDeactivated(@NotNull final InternalFrameEvent e) {
485            // ignore
486        }
487
488        @Override
489        public void internalFrameDeiconified(@NotNull final InternalFrameEvent e) {
490            // ignore
491        }
492
493        @Override
494        public void internalFrameIconified(@NotNull final InternalFrameEvent e) {
495            mapViewFocusLostNotify(mapView);
496        }
497
498        @Override
499        public void internalFrameOpened(@NotNull final InternalFrameEvent e) {
500            // ignore
501        }
502
503    }
504
505}