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}