/** * Copyright (C) 2002-2022 The FreeCol Team * * This file is part of FreeCol. * * FreeCol is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * FreeCol is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with FreeCol. If not, see . */ package net.sf.freecol.client.gui; import static net.sf.freecol.common.util.CollectionUtils.none; import static net.sf.freecol.common.util.CollectionUtils.transform; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.GraphicsDevice; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JComponent; import javax.swing.JDesktopPane; import javax.swing.JInternalFrame; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JMenuBar; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.border.EmptyBorder; import javax.swing.plaf.basic.BasicInternalFrameUI; import net.sf.freecol.client.ClientOptions; import net.sf.freecol.client.FreeColClient; import net.sf.freecol.client.gui.SwingGUI.PopupPosition; import net.sf.freecol.client.gui.action.FreeColAction; import net.sf.freecol.client.gui.dialog.FreeColDialog; import net.sf.freecol.client.gui.mapviewer.CanvasMapViewer; import net.sf.freecol.client.gui.mapviewer.MapViewer; import net.sf.freecol.client.gui.menu.InGameMenuBar; import net.sf.freecol.client.gui.menu.MapEditorMenuBar; import net.sf.freecol.client.gui.menu.MenuMouseMotionListener; // Special case panels, TODO: can we move these to Widgets? import net.sf.freecol.client.gui.panel.FreeColPanel; import net.sf.freecol.client.gui.panel.MainPanel; import net.sf.freecol.client.gui.panel.MapControls; import net.sf.freecol.client.gui.panel.MapEditorTransformPanel; import net.sf.freecol.client.gui.panel.Utility; import net.sf.freecol.client.gui.video.VideoComponent; import net.sf.freecol.client.gui.video.VideoListener; import net.sf.freecol.common.i18n.Messages; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.option.IntegerOption; import net.sf.freecol.common.option.Option; import net.sf.freecol.common.option.OptionGroup; import net.sf.freecol.common.resources.Video; /** * The main container for the other GUI components in FreeCol. * This is where lower level graphics coordination occurs. * Specific panels and dialogs are over in Widgets * (TODO) with a few exceptions. */ public final class Canvas extends JDesktopPane { public static final String PROPERTY_RESIZABLE = "Canvas.resizable"; public static final String PROPERTY_POPUP_POSITION = "Canvas.popupPosition"; private static final Logger logger = Logger.getLogger(Canvas.class.getName()); /** Number of tries to find a clear spot on the canvas. */ private static final int MAXTRY = 3; /** The cursor to show for goto operations. */ public static final java.awt.Cursor GO_CURSOR = (java.awt.Cursor)UIManager.get("cursor.go"); /** A class for frames being used as tool boxes. */ private static class ToolBoxFrame extends JInternalFrame {} /** The game client. */ private final FreeColClient freeColClient; /** The graphics device to display to. */ private final GraphicsDevice graphicsDevice; /** Is the canvas in windowed mode? */ private boolean windowed; /** The parent frame, either a window or the full screen. */ private FreeColFrame parentFrame; /** Remember the current size (from getSize()), check for changes. */ private Dimension oldSize; /** * The component that displays the map. * Ideally this would be completely separate, but the displayMap * (and possibly changeSize) call/s means we must retain a reference * to the map viewer. * We also use it for the Cursor, which remains in MapViewer so * that displayMap can draw it when active needed. */ private final MapViewer mapViewer; /** The various sorts of map controls. */ private MapControls mapControls; /** The special overlay used when it is not the player turn. */ private GrayLayer greyLayer; /** The dialogs in view. */ private final List> dialogs = new ArrayList<>(); /** * The main panel. Remember this because getExistingFreeColPanel * gets confused across switches between the game and the map editor * which makes it hard to remove. */ private MainPanel mainPanel; private Scrolling scrolling; /** * The panel used for displaying the map and drawing the background of this class. */ private CanvasMapViewer canvasMapViewer; /** * A timer triggering repaints in order to display animations on the {@link CanvasMapViewer}. */ private Timer animationTimer; /** * The constructor to use. * * @param freeColClient The {@code FreeColClient} for the game. * @param graphicsDevice The {@code GraphicsDevice} to display on. * @param desiredSize The desired size of the parent frame. * @param mapViewer The object responsible of drawing the map. * @param mapControls The controls on the map. */ public Canvas(final FreeColClient freeColClient, final GraphicsDevice graphicsDevice, final Dimension desiredSize, MapViewer mapViewer, MapControls mapControls) { assert SwingUtilities.isEventDispatchThread(); this.freeColClient = freeColClient; this.graphicsDevice = graphicsDevice; this.canvasMapViewer = new CanvasMapViewer(freeColClient, mapViewer); this.scrolling = new Scrolling(freeColClient, this); // Determine if windowed mode should be used and set the window size. this.windowed = checkWindowed(graphicsDevice, desiredSize); Rectangle windowBounds = null; if (this.windowed && desiredSize != null && desiredSize.width > 0 && desiredSize.height > 0) { windowBounds = new Rectangle(desiredSize); } this.parentFrame = createFrame(null, windowBounds); this.oldSize = getSize(); add(canvasMapViewer, FRAME_CONTENT_LAYER); canvasMapViewer.setSize(oldSize); canvasMapViewer.setLocation(0, 0); addComponentListener(new ComponentListener() { @Override public void componentResized(ComponentEvent e) { updateSize(); } @Override public void componentMoved(ComponentEvent e) { updateSize(); } @Override public void componentShown(ComponentEvent e) { updateSize(); } @Override public void componentHidden(ComponentEvent e) { updateSize(); } }); this.mapViewer = mapViewer; this.mapControls = mapControls; this.greyLayer = new GrayLayer(freeColClient); setDoubleBuffered(true); setOpaque(true); setLayout(null); setFocusable(true); setFocusTraversalKeysEnabled(false); // Create key bindings for all actions for (Option option : freeColClient.getActionManager().getOptions()) { FreeColAction action = (FreeColAction)option; getInputMap().put(action.getAccelerator(), action.getId()); getActionMap().put(action.getId(), action); } this.parentFrame.setVisible(true); updateSize(); revalidate(); repaint(); /* * TODO: Stop using hardcoded 125ms for animation. Instead, we should * define timing for every terrain animation. */ animationTimer = new Timer(125, (event) -> { paintJustTheMapImmediately(); }); this.animationTimer.start(); logger.info("Canvas created with bounds: " + windowBounds); } // Internals private void updateSize() { Dimension size = getSize(); if (oldSize.width != size.width || oldSize.height != size.height) { logger.info("Canvas resize from " + oldSize + " to " + size); oldSize = size; canvasMapViewer.changeSize(size); if (removeMapControls()) { freeColClient.getGUI().updateMapControls(); addMapControls(); } updateFrameSizesAndPositions(size); revalidate(); repaint(); } } private void updateFrameSizesAndPositions(Dimension canvasSize) { for (Component c : getComponents()) { if (!(c instanceof JInternalFrame)) { continue; } /* * Determines a new sensible size. */ final JInternalFrame f = (JInternalFrame) c; final Boolean resizeable = (Boolean) f.getClientProperty(PROPERTY_RESIZABLE); final Dimension newSize; if (resizeable != null && resizeable) { newSize = capSizeToMaximum(f, canvasSize); f.setSize(newSize); } else { newSize = f.getSize(); } /* * Moves frames so that they are no longer out-of-bounds of the Canvas. */ final Point loc = f.getLocation(); final int newX = (loc.x + newSize.width > canvasSize.width) ? canvasSize.width - newSize.width : loc.x; final int newY = (loc.y + newSize.height > canvasSize.height) ? canvasSize.height - newSize.height : loc.y; f.setLocation(new Point(Math.max(0, newX), Math.max(0, newY))); /* * Maintains logical positions (like centered) after resize. */ final PopupPosition popupPosition = (PopupPosition) f.getClientProperty(PROPERTY_POPUP_POSITION); if (popupPosition == null) { continue; } final Point p = chooseLocation(f, f.getWidth(), f.getHeight(), popupPosition); f.setLocation(p); } } private Dimension capSizeToMaximum(JInternalFrame f, Dimension maxSize) { final int FRAME_EMPTY_SPACE = 60; final int width = Math.max(f.getMinimumSize().width, Math.min(f.getWidth(), maxSize.width - FRAME_EMPTY_SPACE)); final int height = Math.max(f.getMinimumSize().height, Math.min(f.getHeight(), maxSize.height - FRAME_EMPTY_SPACE)); return new Dimension(width, height); } // Frames and Windows /** * Toggle windowed flag. */ private void toggleWindowed() { this.windowed = !this.windowed; } /** * Create a new frame with a given menu bar and bounds. * * @param menuBar The new frames {@code JMenuBar}. * @param windowBounds The new frame bounding {@code Rectangle}. * @return The new {@code FreeColFrame}. */ private FreeColFrame createFrame(JMenuBar menuBar, Rectangle windowBounds) { // FIXME: Check this: // User might have moved window to new screen in a // multi-screen setup, so make this.gd point to the current screen. FreeColFrame fcf = new FreeColFrame(this.freeColClient, this.graphicsDevice, menuBar, isWindowed(), windowBounds); fcf.getContentPane().add(this); return fcf; } /** * Destroy the current frame. */ private void destroyFrame() { if (this.parentFrame != null) { this.parentFrame.setVisible(false); if (!isWindowed()) this.parentFrame.exitFullScreen(); this.parentFrame.dispose(); this.parentFrame = null; } } /** * Determine whether to use full screen or windowed mode. * * @param gd The {@code GraphicsDevice} to display to. * @param desiredSize An optional window {@code Dimension}. * @return True if windowed mode is to be used, false for full screen. */ private static boolean checkWindowed(GraphicsDevice gd, Dimension desiredSize) { boolean ret; if (desiredSize == null) { if (gd.isFullScreenSupported()) { logger.info("Full screen mode used."); ret = false; } else { logger.warning("Full screen mode not supported."); System.err.println(Messages.message("client.fullScreen")); ret = true; } } else { logger.info("Windowed mode used."); ret = true; } return ret; } /** * Adds a component on this Canvas inside a frame. * * @param comp The component to add to the canvas. * @param toolBox Should be set to true if the resulting frame is * used as a toolbox (that is: it should not be counted as a * frame). * @param popupPosition A preferred {@code PopupPosition}. * @param resizable Whether this component can be resized. * @return The {@code JInternalFrame} that was created and added. */ private JInternalFrame addAsFrame(JComponent comp, boolean toolBox, PopupPosition popupPosition, boolean resizable) { final JInternalFrame f = (toolBox) ? new ToolBoxFrame() : new JInternalFrame(); Container con = f.getContentPane(); if (con instanceof JComponent) { JComponent c = (JComponent)con; c.setOpaque(false); c.setBorder(null); } if (comp.getBorder() != null) { if (comp.getBorder() instanceof EmptyBorder) { f.setBorder(Utility.blankBorder(10, 10, 10, 10)); } else { f.setBorder(comp.getBorder()); comp.setBorder(Utility.blankBorder(5, 5, 5, 5)); } } else { f.setBorder(null); } final FrameMotionListener fml = new FrameMotionListener(f); comp.addMouseMotionListener(fml); comp.addMouseListener(fml); if (f.getUI() instanceof BasicInternalFrameUI) { BasicInternalFrameUI biu = (BasicInternalFrameUI) f.getUI(); biu.setNorthPane(null); biu.setSouthPane(null); biu.setWestPane(null); biu.setEastPane(null); } f.getContentPane().add(comp); f.setOpaque(false); f.pack(); final Dimension size = capSizeToMaximum(f, getSize()); f.setSize(size); final Point p = chooseLocation(comp, size.width, size.height, popupPosition); final Point adjustedP = adjustLocationForClearSpace(p, size.width, size.height); if (popupPosition == PopupPosition.CENTERED_FORCED || p.equals(adjustedP)) { f.setLocation(p); f.putClientProperty(PROPERTY_POPUP_POSITION, popupPosition); f.putClientProperty(PROPERTY_RESIZABLE, resizable); } else { f.setLocation(adjustedP); } this.addToCanvas(f, MODAL_LAYER); f.setName(comp.getClass().getSimpleName()); f.setFrameIcon(null); f.setVisible(true); f.setResizable(resizable); try { f.setSelected(true); } catch (java.beans.PropertyVetoException e) {} return f; } /** * Adds a component centered on this Canvas. * * @param comp The {@code Component} to add to this canvas. * @param layer The layer to add the component to (see JLayeredPane). */ private void addCentered(Component comp, Integer layer) { comp.setLocation((this.oldSize.width - comp.getWidth()) / 2, (this.oldSize.height - comp.getHeight()) / 2); addToLayer(comp, layer); } /** * Adds a component to this Canvas. * * @param comp The {@code Component} to add to this canvas. * @param layer The layer to add the component to (see JLayeredPane). */ private void addToLayer(Component comp, Integer layer) { addToCanvas(comp, layer); updateMenuBar(); } /** * Adds a component to this Canvas updating the menus. * * @param comp The {@code Component} to add to this canvas. * @param layer The layer to add the component to (see JLayeredPane). */ private void addToCanvas(Component comp, Integer layer) { try { // To avoid illegal component position exception - remove the component first remove(comp); add(comp, layer); } catch (Exception e) { logger.log(Level.WARNING, "addToCanvas(" + comp.getClass().getSimpleName() + " at " + comp.getX() + "," + comp.getY() + " on layer " + layer + ") failed.", e); } } /** * Gets the internal frame for the given component. * * @param c The {@code Component}. * @return The given component if this is an internal frame or the * first parent that is an internal frame. Returns * {@code null} if no internal frame is found. */ private JInternalFrame getInternalFrame(final Component c) { Component temp = c; while (temp != null && !(temp instanceof JInternalFrame)) { temp = temp.getParent(); } return (JInternalFrame) temp; } /** * Is nothing showing? * * @return True if no dialog or panel is being shown. */ private boolean nothingShowing() { return this.dialogs.isEmpty() && getShowingPanel() == null; } // Location, choice and persistence /** * Choose a location for a component. * * @param comp The {@code Component} to place. * @param width The component width to use. * @param height The component height to use. * @param popupPosition An optional {@code PopupPosition} hint. * @return A suitable {@code Point} to place the component. */ private Point chooseLocation(Component comp, int width, int height, PopupPosition popupPosition) { Point p = null; if ((comp instanceof FreeColPanel) && (p = getSavedPosition(comp)) != null) { // Sanity check stuff coming out of client options. if (p.getX() < 0 || p.getX() >= getWidth() - width || p.getY() < 0 || p.getY() >= getHeight() - height) { p = null; } } int x = 0, y = 0; if (p != null) { x = (int)p.getX(); y = (int)p.getY(); } else if (popupPosition != null) { switch (popupPosition) { case CENTERED: case CENTERED_FORCED: x = (getWidth() - width) / 2; y = (getHeight() - height) / 2; break; case CENTERED_LEFT: x = (getWidth() - width) / 4; y = (getHeight() - height) / 2; break; case CENTERED_RIGHT: x = ((getWidth() - width) * 3) / 4; y = (getHeight() - height) / 2; break; case ORIGIN: x = y = 0; break; } } return new Point(x, y); } private Point adjustLocationForClearSpace(Point location, int width, int height) { Point p; int x = location.x; int y = location.y; if ((p = getClearSpace(x, y, width, height, MAXTRY)) != null && p.x >= 0 && p.x < getWidth() && p.y >= 0 && p.y < getHeight()) { x = p.x; y = p.y; } return new Point(x, y); } /** * Try to find some free space on the canvas for a component, * starting at x,y. * * @param x A starting x coordinate. * @param y A starting y coordinate. * @param w The component width to use. * @param h The component height to use. * @param tries The number of attempts to find a clear space. * @return A {@code Point} to place the component at or null * on failure. */ private Point getClearSpace(final int x, final int y, final int w, final int h, int tries) { final Rectangle bounds = this.getBounds(); if (!bounds.contains(x, y)) return null; tries = 3 * tries + 1; // 3 new candidates per level List todo = new ArrayList<>(); Point p = new Point(x, y); todo.add(p); List allComponents = transform(this.getComponents(), c -> !(c instanceof GrayLayer) && c.isValid()); allComponents.addAll(dialogs); // Find the position with the least overlap int bestScore = Integer.MAX_VALUE; Point best = p; while (!todo.isEmpty()) { p = todo.remove(0); Rectangle r = new Rectangle(p.x, p.y, w, h); if (!bounds.contains(r)) { continue; } // Find the most overlapping component at this position, // as well as the globally least. int foundScore = 0; Component found = null; for (Component c : allComponents) { Rectangle rb = c.getBounds(); if (rb.intersects(r)) { Rectangle rr = rb.intersection(r); int score = (int)Math.round(rr.getWidth() * rr.getHeight()); if (foundScore < score) { foundScore = score; found = c; } } } if (found == null) { // Can not improve on no overlap, return now return p; } if (bestScore > foundScore) { bestScore = foundScore; best = p; } // Guarantee eventual completion if (--tries <= 0) break; int n = todo.size(), // Some alternative new positions // 0: move right/down to avoid the collision // 1: move as far as possible right/down // 2: wrap back to the far left x0 = found.getX() + found.getWidth() + 1, y0 = found.getY() + found.getHeight() + 1, x1 = bounds.x + bounds.width - w - 1, y1 = bounds.y + bounds.height - h - 1, x2 = bounds.x, y2 = bounds.y; boolean x0ok = bounds.contains(x0 + w, y), y0ok = bounds.contains(x, y0 + h), x1ok = bounds.contains(x1, y), y1ok = bounds.contains(x, y1); todo.add(n, new Point((x0ok) ? x0 : (x1ok) ? x1 : x2, (y0ok) ? y0 : (y1ok) ? y1 : y2)); todo.add(n, new Point(x, (y0ok) ? y0 : (y1ok) ? y1 : y2)); todo.add(n, new Point((x0ok) ? x0 : (x1ok) ? x1 : x2, y)); } return best; } /** * Gets the saved position of a component. * * @param comp The {@code Component} to use. * @return The saved position as a {@code Point}, or null if no * saved position is found. */ private Point getSavedPosition(Component comp) { final ClientOptions co = this.freeColClient.getClientOptions(); if (co == null) return null; try { if (!co.getBoolean(ClientOptions.REMEMBER_PANEL_POSITIONS)) { return null; } } catch (Exception e) {} return co.getPanelPosition(comp.getClass().getName()); } /** * Get the saved size of a component. * * @param comp The {@code Component} to use. * @return A {@code Dimension} for the component or null if * no saved size is found. */ private Dimension getSavedSize(Component comp) { final ClientOptions co = this.freeColClient.getClientOptions(); if (co == null) return null; try { if (!co.getBoolean(ClientOptions.REMEMBER_PANEL_SIZES)) { return null; } } catch (Exception e) {} return co.getPanelSize(comp.getClass().getName()); } /** * Save an {@code int} value to the saved ClientOptions, * using the name of the components class plus the given key as * and identifier. * * @param className The class name for the component. * @param key The key to save. * @param value The value to save. */ private void saveInteger(String className, String key, int value) { if (this.freeColClient == null) return; final ClientOptions co = this.freeColClient.getClientOptions(); if (co == null) return; final OptionGroup etc = co.getOptionGroup(ClientOptions.ETC); if (etc == null) return; // Insist the option is present if (!etc.hasOption(className + key, IntegerOption.class)) { Specification specification = (this.freeColClient.getGame() == null) ? null : this.freeColClient.getGame().getSpecification(); etc.add(new IntegerOption(className + key, specification)); } // Set the value etc.setInteger(className + key, value); } /** * Save the position of a component. * * @param comp The {@code Component} to use. * @param position The position to save. */ private void savePosition(Component comp, Point position) { try { if (!this.freeColClient.getClientOptions() .getBoolean(ClientOptions.REMEMBER_PANEL_POSITIONS)) return; } catch (Exception e) {} String className = comp.getClass().getName(); saveInteger(className, ".x", position.x); saveInteger(className, ".y", position.y); } /** * Save the size of a component. * * @param comp The {@code Component} to use. * @param size The {@code Dimension} to save. */ private void saveSize(Component comp, Dimension size) { try { if (!this.freeColClient.getClientOptions() .getBoolean(ClientOptions.REMEMBER_PANEL_SIZES)) return; } catch (Exception e) {} String className = comp.getClass().getName(); saveInteger(className, ".w", size.width); saveInteger(className, ".h", size.height); } /** * A component is closing. Some components need position and size * to be saved. * * @param c The closing {@code Component}. * @param jif The enclosing {@code JInternalFrame}. */ private void notifyClose(Component c, JInternalFrame jif) { if (c instanceof FreeColPanel) { FreeColPanel fcp = (FreeColPanel)c; fcp.firePropertyChange("closing", false, true); savePosition(fcp, jif.getLocation()); saveSize(fcp, fcp.getSize()); if (nothingShowing()) updateMenuBar(); } } // Public routines follow // Initialization and teardown /** * Removes components that is only used when in game. */ public void removeInGameComponents() { // remove listeners, they will be added when launching the new game... removeKeyAndMouseListeners(); for (Component c : getComponents()) { if (c instanceof CanvasMapViewer) { continue; } removeFromCanvas(c); } } private void removeKeyAndMouseListeners() { KeyListener[] keyListeners = getKeyListeners(); for (KeyListener keyListener : keyListeners) { removeKeyListener(keyListener); } MouseListener[] mouseListeners = getMouseListeners(); for (MouseListener mouseListener : mouseListeners) { removeMouseListener(mouseListener); } MouseMotionListener[] mouseMotionListeners = getMouseMotionListeners(); for (MouseMotionListener mouseMotionListener : mouseMotionListeners) { removeMouseMotionListener(mouseMotionListener); } } /** * Map editor initialization. */ public void startMapEditorGUI() { removeKeyAndMouseListeners(); freeColClient.updateActions(); this.parentFrame.setMenuBar(new MapEditorMenuBar(this.freeColClient, new MenuMouseMotionListener(scrolling))); CanvasMapEditorMouseListener listener = new CanvasMapEditorMouseListener(this.freeColClient, scrolling); addMouseListener(listener); addMouseMotionListener(listener); } /** * In game initializations. */ public void initializeInGame() { removeKeyAndMouseListeners(); this.parentFrame.setMenuBar(new InGameMenuBar(this.freeColClient, new MenuMouseMotionListener(scrolling))); addMouseListener(new CanvasMouseListener(this.freeColClient)); addMouseMotionListener(new CanvasMouseMotionListener(this.freeColClient, scrolling)); } /** * Quit the canvas. */ public void quit() { destroyFrame(); } /** * Set preferred size to saved size, or to the given * {@code Dimension} if no saved size was found. Call this * method in the constructor of a FreeColPanel in order to * remember its size and position. * * @param comp The {@code Component} to use. * @param d The {@code Dimension} to use as default. */ public void restoreSavedSize(Component comp, Dimension d) { final Dimension pref = comp.getPreferredSize(); final Dimension sugg = (d == null) ? pref : d; boolean save = false; Dimension size = getSavedSize(comp); if (size == null) { size = new Dimension(pref); save = true; } // Fix up broken/outdated saved sizes if(size.width < sugg.width) { size.width = sugg.width; save = true; } if(size.height < sugg.height) { size.height = sugg.height; save = true; } if(size.width < pref.width) { size.width = pref.width; save = true; } if(size.height < pref.height) { size.height = pref.height; save = true; } if(save) { saveSize(comp, size); } if (!pref.equals(size)) { comp.setPreferredSize(size); } } // Animation handling /** * Add and remove animation labels. * * @param label A {@code JLabel} for an animation. * @param add If true, add the label, else remove it. */ public void animationLabel(JLabel label, boolean add) { if (add) { addToCanvas(label, JLayeredPane.DEFAULT_LAYER); } else { removeFromCanvas(label); } } // Dialog and panel primitives, several only public for Widgets /** * Close a panel by class name. * * @param panel The panel to close. */ public void closePanel(String panel) { if (panel.endsWith("Panel")) { for (Component c1 : getComponents()) { if (c1 instanceof JInternalFrame) { for (Component c2 : ((JInternalFrame)c1).getContentPane() .getComponents()) { if (panel.equals(c2.getClass().getName())) { notifyClose(c2, (JInternalFrame)c1); return; } } } } } else if (panel.endsWith("Dialog")) { for (FreeColDialog fcd : new ArrayList<>(dialogs)) { if (panel.equals(fcd.getClass().getName())) { dialogs.remove(fcd); fcd.dispose(); return; } } } } /** * Add a dialog to the current dialog list. * * @param fcd The dialog to add. */ public void dialogAdd(FreeColDialog fcd) { if (fcd.isModal()) { this.mapViewer.getMapViewerState().setCursorBlinking(false); } dialogs.add(fcd); } /** * Remove a dialog from the current dialog list. * * @param fcd The dialog to remove. */ public void dialogRemove(FreeColDialog fcd) { dialogs.remove(fcd); if (fcd.isModal() && none(dialogs, FreeColDialog::isModal)) { this.mapViewer.getMapViewerState().setCursorBlinking(true); } if (nothingShowing()) updateMenuBar(); } /** * Get a currentlydisplayed FreeColDialog of a given type. * * @param type The class of dialog to look for. * @return The {@code FreeColDialog} found, or null if none present. */ public FreeColDialog getExistingFreeColDialog(Class type) { for (FreeColDialog d : dialogs) { if (d.getClass() == type) return d; } return null; } /** * Gets a currently displayed FreeColPanel of a given type. * * @param The actual panel type. * @param type The type of {@code FreeColPanel} to look for. * @return A currently displayed {@code FreeColPanel} of the * requested type, or null if none found. */ public T getExistingFreeColPanel(Class type) { for (Component c1 : getComponents()) { if (c1 instanceof JInternalFrame) { for (Component c2 : ((JInternalFrame)c1).getContentPane() .getComponents()) { try { T ret = type.cast(c2); if (ret != null) { final JInternalFrame jif = (JInternalFrame)c1; SwingUtilities.invokeLater(() -> { jif.toFront(); jif.repaint(); }); return ret; } } catch (ClassCastException cce) {} } } } return null; } /** * Gets any currently displayed component matching a predicate. * * @param pred The predicate to apply. * @return The first match for the predicate, or null if none found. */ public Component getMatchingComponent(Predicate pred) { for (Component c1 : getComponents()) { if (c1 instanceof JInternalFrame) { for (Component c2 : ((JInternalFrame) c1).getContentPane() .getComponents()) { if (pred.test(c2)) return c2; } } } return null; } /** * Get any panel this {@code Canvas} is displaying. * * @return A {@code Component} the {@code Canvas} is * displaying, or null if none found. */ public Component getShowingPanel() { for (Component c : getComponents()) { if (c instanceof ToolBoxFrame) { continue; } if (c instanceof JInternalFrame) { return c; } else if (c instanceof JInternalFrame.JDesktopIcon) { return c; } } return null; } /** * Removes the given component from this canvas without * updating the menu bar. * * @param comp The {@code Component} to remove. */ public void removeFromCanvas(Component comp) { if (comp == null) return; final Rectangle updateBounds = comp.getBounds(); final JInternalFrame jif = getInternalFrame(comp); if (jif != null && jif != comp) { jif.dispose(); } else { // Java 1.7.0 as seen on Fedora with: // Java version: 1.7.0_40 // Java WM version: 24.0-b56 // crashes here deep in the java libraries. try { super.remove(comp); } catch (Exception e) { logger.log(Level.WARNING, "Java crash", e); } } if (jif != null) { // Notify close after removing from Canvas notifyClose(comp, jif); } repaint(updateBounds.x, updateBounds.y, updateBounds.width, updateBounds.height); } /** * Displays the given dialog, optionally making sure a tile is visible. * * @param The type to be returned from the dialog. * @param dialog The {@code FreeColDialog} to be displayed * @param pos A {@code PopupPosition} for the dialog. * @return The {@link FreeColDialog#getResponse reponse} returned by * the dialog. */ public T showFreeColDialog(FreeColDialog dialog, PopupPosition pos) { viewFreeColDialog(dialog, pos); T response = dialog.getResponse(); remove(dialog); dialogRemove(dialog); return response; } /** * Displays a {@code FreeColPanel} at a generalized position. * * @param panel {@code FreeColPanel}, panel to show * @param popupPosition {@code PopupPosition} The generalized * position to place the panel. * @param resizable Should the panel be resizable? * @return The panel. */ public FreeColPanel showFreeColPanel(FreeColPanel panel, PopupPosition popupPosition, boolean resizable) { repaint(); addAsFrame(panel, false, popupPosition, resizable); panel.requestFocus(); freeColClient.getActionManager().update(); return panel; } /** * Displays the given dialog, optionally making sure a tile is visible. * * @param The type to be returned from the dialog. * @param freeColDialog The dialog to be displayed * @param pos A {@code PopupPosition} for the dialog. */ public void viewFreeColDialog(final FreeColDialog freeColDialog, PopupPosition pos) { // TODO: Remove compatibility code when all non-modal dialogs // have been converted into panels. if (!freeColDialog.isModal()) { int canvasWidth = getWidth(); int dialogWidth = freeColDialog.getWidth(); if (dialogWidth*2 <= canvasWidth) { Point location = freeColDialog.getLocation(); if (pos == PopupPosition.CENTERED_LEFT) { freeColDialog.setLocation(location.x - canvasWidth/4, location.y); } else if (pos == PopupPosition.CENTERED_RIGHT) { freeColDialog.setLocation(location.x + canvasWidth/4, location.y); } } } dialogAdd(freeColDialog); freeColDialog.requestFocus(); freeColDialog.setVisible(true); } // Frames and windowing /** * Are we in windowed mode? * * @return True if in windowed mode. */ public boolean isWindowed() { return this.windowed; } /** * Get the parent frame. * * Do not use this except inside Widgets (which is really just an * extension of Canvas). * * @return The parent {@code FreeColFrame}. */ public FreeColFrame getParentFrame() { return this.parentFrame; } /** * Toggle the frame between windowed and non-windowed modes. */ public void toggleFrame() { JMenuBar menuBar = null; Rectangle windowBounds = null; if (this.parentFrame != null) { menuBar = this.parentFrame.getJMenuBar(); windowBounds = this.parentFrame.getBounds(); } destroyFrame(); toggleWindowed(); this.parentFrame = createFrame(menuBar, windowBounds); this.parentFrame.setVisible(true); } // Map controls, called out of paintComponent via checkResize /** * Add the map controls. * * @return True if any were added. */ public boolean addMapControls() { if (this.mapControls == null) return false; List components = this.mapControls.getComponentsToAdd(this.oldSize); for (Component c : components) { addToCanvas(c, JLayeredPane.MODAL_LAYER); } return !components.isEmpty(); } /** * Remove the map controls. * * @return True if any were removed. */ public boolean removeMapControls() { if (mapControls == null) { return false; } final List components = mapControls.getComponentsPresent(); boolean ret = false; for (Component c : components) { removeFromCanvas(c); ret = true; } mapControls.clear(); return ret; } // Menus /** * Closes all the menus that are currently open. */ public void closeMenus() { for (JInternalFrame jif : getAllFrames()) { for (Component c : jif.getContentPane().getComponents()) { notifyClose(c, jif); } jif.dispose(); } while (!dialogs.isEmpty()) { FreeColDialog dialog = dialogs.remove(0); dialog.dispose(); } } /** * Reset the menu bar. */ public void resetMenuBar() { this.freeColClient.updateActions(); this.parentFrame.resetMenuBar(); } /** * Update the menu bar. */ public void updateMenuBar() { this.freeColClient.updateActions(); this.parentFrame.updateMenuBar(); } // Video /** * Play the opening video. * * @param videoId An identifier for the video content. * @param muteAudio Mute if true. * @param runnable A {@code Runnable} to run on completion. */ public void playVideo(String videoId, boolean muteAudio, final Runnable runnable) { final Video video = ImageLibrary.getVideo(videoId); if (video == null) { runnable.run(); return; } final String originalVendor = System.getProperty("java.vendor"); if (originalVendor.indexOf(" ") == -1) { /* Cortado crashes unless there is a space in "java.vendor". */ System.setProperty("java.vendor", originalVendor + " cortadoBugFix"); } final VideoComponent vc = new VideoComponent(video, muteAudio, getSize()); final class AbortListener implements ActionListener, KeyListener, MouseListener, VideoListener { private Timer t = null; @Override public void keyPressed(KeyEvent e) { } @Override public void keyReleased(KeyEvent e1) { execute(); } @Override public void keyTyped(KeyEvent e2) { } @Override public void mouseClicked(MouseEvent e3) { execute(); } @Override public void mouseEntered(MouseEvent e4) { } @Override public void mouseExited(MouseEvent e5) { } @Override public void mousePressed(MouseEvent e6) { } @Override public void mouseReleased(MouseEvent e7) { } @Override public void stopped() { execute(); } @Override public void actionPerformed(ActionEvent ae) { // from timer execute(); } public void startTimer(int tim) { this.t = new Timer(tim, this); this.t.setRepeats(false); this.t.start(); } private void execute() { removeKeyListener(this); removeMouseListener(this); vc.removeMouseListener(this); vc.stop(); remove(vc); if (t != null) { t.stop(); } System.setProperty("java.vendor", originalVendor); runnable.run(); } } closeMenus(); // Clicks or keyboard on the canvas and the video stop the video AbortListener l = new AbortListener(); addMouseListener(l); addKeyListener(l); vc.addMouseListener(l); vc.addKeyListener(l); // The Cortado applet is failing to quit when finished, make // sure it eventually gets kicked. // Change the magic number if we change the opening video length. l.startTimer(80000); // Add video and play addCentered(vc, JLayeredPane.PALETTE_LAYER); vc.play(); } // Special dialogs and panels /** * Closes the {@link MainPanel}. */ public void closeMainPanel() { if (this.mainPanel != null) { remove(this.mainPanel); this.mainPanel = null; } } /** * Closes all panels, changes the background and shows the main menu. */ public void mainTitle() { showMainPanel(); revalidate(); repaint(); } void mainTitleIfMainPanelIsAlreadyShowing() { if (mainPanel != null && mainPanel.isShowing()) { mainTitle(); } } /** * Shows the {@code MainPanel}. * * @return The main panel. */ public FreeColPanel showMainPanel() { prepareShowingMainMenu(); this.mainPanel = new MainPanel(this.freeColClient); addAsFrame(mainPanel, false, PopupPosition.CENTERED_FORCED, false); this.mainPanel.requestFocus(); return this.mainPanel; } public void prepareShowingMainMenu() { removeInGameComponents(); closeMenus(); closeMainPanel(); this.parentFrame.removeMenuBar(); } /** * Display the map editor transform panel. */ public void showMapEditorTransformPanel() { MapEditorTransformPanel panel = new MapEditorTransformPanel(this.freeColClient); JInternalFrame f = addAsFrame(panel, true, PopupPosition.CENTERED, false); f.setLocation(f.getX(), 50); // move up to near the top repaint(); } // Override JComponent public void paintJustTheMapImmediately() { if (this.freeColClient == null || this.freeColClient.getGame() == null || this.freeColClient.getGame().getMap() == null) { return; } // TODO: Allow terrain animations to be turned off. /* * We can change to active rendering using: * final Graphics2D g2d = (Graphics2D) getGraphics(); final Dimension size = getSize(); g2d.setClip(0, 0, size.width, size.height); this.mapViewer.displayMap(g2d, size); g2d.dispose(); * ...but then we also need to call the methods in this component, * for example paintChildren. */ canvasMapViewer.paintImmediately(); } /** * {@inheritDoc} */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); /* * TODO: We should not manipulate the component tree here. */ if (!this.freeColClient.isInGame()) { return; } final boolean hasMap = this.freeColClient != null && this.freeColClient.getGame() != null && this.freeColClient.getGame().getMap() != null; if (hasMap) { final Dimension size = getSize(); // toggle grey layer if needed if (this.freeColClient.currentPlayerIsMyPlayer()) { if (greyLayer.getParent() != null) { removeFromCanvas(greyLayer); } } else { greyLayer.setBounds(0, 0, size.width, size.height); greyLayer.setPlayer(this.freeColClient.getGame() .getCurrentPlayer()); if (greyLayer.getParent() == null) { addToCanvas(greyLayer, JLayeredPane.DRAG_LAYER); } } } } // Override Container /** * {@inheritDoc} */ @Override public void remove(Component comp) { removeFromCanvas(comp); } }