/******************************************************************************* * Copyright (c) 2015 - 2017 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package jsettlers.mapcreator.control; import go.graphics.area.Area; import go.graphics.region.Region; import go.graphics.swing.AreaContainer; import go.graphics.swing.sound.SwingSoundPlayer; import jsettlers.common.CommonConstants; import jsettlers.common.buildings.EBuildingType; import jsettlers.common.landscape.ELandscapeType; import jsettlers.common.map.MapLoadException; import jsettlers.common.menu.FakeMapGame; import jsettlers.common.menu.IMapInterfaceListener; import jsettlers.common.menu.action.EActionType; import jsettlers.common.menu.action.IAction; import jsettlers.common.position.ShortPoint2D; import jsettlers.common.resources.ResourceManager; import jsettlers.exceptionhandler.ExceptionHandler; import jsettlers.graphics.action.ActionFireable; import jsettlers.graphics.action.PointAction; import jsettlers.graphics.map.MapContent; import jsettlers.graphics.map.MapInterfaceConnector; import jsettlers.graphics.map.ETextDrawPosition; import jsettlers.logic.map.loading.MapLoader; import jsettlers.logic.map.loading.list.MapList; import jsettlers.logic.map.loading.newmap.MapFileHeader; import jsettlers.main.swing.SwingManagedJSettlers; import jsettlers.mapcreator.data.MapData; import jsettlers.mapcreator.data.MapDataDelta; import jsettlers.mapcreator.localization.EditorLabels; import jsettlers.mapcreator.main.action.AbortDrawingAction; import jsettlers.mapcreator.main.action.CombiningActionFirerer; import jsettlers.mapcreator.main.action.DrawLineAction; import jsettlers.mapcreator.main.action.EndDrawingAction; import jsettlers.mapcreator.main.action.StartDrawingAction; import jsettlers.mapcreator.main.map.MapEditorControls; import jsettlers.mapcreator.main.window.EditorFrame; import jsettlers.mapcreator.main.window.LastUsedHandler; import jsettlers.mapcreator.main.window.NewFileDialog; import jsettlers.mapcreator.main.window.OpenExistingDialog; import jsettlers.mapcreator.main.window.SettingsDialog; import jsettlers.mapcreator.main.window.sidebar.RectIcon; import jsettlers.mapcreator.main.window.sidebar.Sidebar; import jsettlers.mapcreator.main.window.sidebar.ToolSidebar; import jsettlers.mapcreator.mapvalidator.AutoFixErrorAction; import jsettlers.mapcreator.mapvalidator.GotoNextErrorAction; import jsettlers.mapcreator.mapvalidator.IScrollToAble; import jsettlers.mapcreator.mapvalidator.ShowErrorsAction; import jsettlers.mapcreator.mapvalidator.ValidationResultListener; import jsettlers.mapcreator.mapvalidator.result.ValidationListModel; import jsettlers.mapcreator.mapvalidator.result.fix.FixData; import jsettlers.mapcreator.mapview.MapGraphics; import jsettlers.mapcreator.stat.StatisticsDialog; import jsettlers.mapcreator.tools.SetStartpointTool; import jsettlers.mapcreator.tools.Tool; import jsettlers.mapcreator.tools.landscape.ResourceTool; import jsettlers.mapcreator.tools.objects.PlaceBuildingTool; import jsettlers.mapcreator.tools.shapes.ShapeType; import javax.swing.AbstractAction; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.SwingUtilities; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Desktop; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.Timer; import java.util.TimerTask; /** * Controller for map editing * * @author Andreas Butti */ public class EditorControl extends EditorControlBase implements IMapInterfaceListener, ActionFireable, IPlayerSetter, IScrollToAble { /** * Map drawing */ private MapGraphics map; /** * Currently active tool */ private Tool tool = null; /** * Currently selected player */ private int currentPlayer = 0; /** * Undo / Redo stack */ private UndoRedoHandler undoRedo; /** * To scrol to positions */ private MapInterfaceConnector connector; /** * Window displayed */ private EditorFrame window; /** * Open GL Contents (Drawing) */ private MapContent mapContent; /** * Sidebar with the tools */ private final ToolSidebar toolSidebar = new ToolSidebar(this) { private static final long serialVersionUID = 1L; @Override protected void changeTool(Tool lastPathComponent) { EditorControl.this.changeTool(lastPathComponent); } }; /** * Sidebar with all tabs */ private final Sidebar sidebar = new Sidebar(toolSidebar, this); /** * Timer for redrawing */ private final Timer redrawTimer = new Timer(true); /** * Action to fix all errors automatically, if clear what to do */ private AutoFixErrorAction autoFixErrorAction; /** * Always display resources */ private boolean showResourcesAlways = false; /** * Show the resources because the current tool requests it */ private boolean showResourcesBecauseOfTool = false; /** * Combobox with the player selection */ private final JComboBox<Integer> playerCombobox = new JComboBox<>(); /** * Constructor */ public EditorControl() { // use heavyweight component playerCombobox.setLightWeightPopupEnabled(false); playerCombobox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { currentPlayer = (Integer) playerCombobox.getSelectedItem(); } }); playerCombobox.setRenderer(new DefaultListCellRenderer() { private static final long serialVersionUID = 1L; @Override public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); Integer player = (Integer) value; setIcon(new RectIcon(22, new Color(mapContent.getPlayerColor(player.byteValue()).getARGB()), Color.GRAY)); setText(String.format(EditorLabels.getLabel("general.player_x"), player)); return this; } }); } /** * Update the player selection combobox */ private void updatePlayerCombobox() { // create a new model, because a swing bug there are sometimes problems updating an existing model DefaultComboBoxModel<Integer> model = new DefaultComboBoxModel<>(); model.setSelectedItem(playerCombobox.getSelectedItem()); for (int i = 0; i < mapData.getPlayerCount(); i++) { model.addElement(i); } playerCombobox.setModel(model); } /** * Load a map * * @param loader * Map to load * @throws MapLoadException */ public void loadMap(MapLoader loader) throws MapLoadException { MapData data = new MapData(loader.getMapData()); MapFileHeader header = loader.getFileHeader(); loadMap(header, data); } /** * Create a new map * * @param header * Header of the file to open * @param ground * Ground to use for the new map */ public void createNewMap(MapFileHeader header, ELandscapeType ground) { loadMap(header, new MapData(header.getWidth(), header.getHeight(), header.getMaxPlayers(), ground)); } /** * Load a map * * @param header * Header to use * @param mapData * Map to use */ public void loadMap(MapFileHeader header, MapData mapData) { setHeader(header); this.mapData = mapData; updatePlayerCombobox(); map = new MapGraphics(mapData); validator.setData(mapData); validator.setHeader(header); validator.addListener(sidebar); buildMapEditingWindow(); new LastUsedHandler().saveUsedMapId(header.getUniqueId()); undoRedo = new UndoRedoHandler(window, mapData); FixData fixData = new FixData(mapData, undoRedo, validator); sidebar.setFixData(fixData); autoFixErrorAction.setFixData(fixData); // Go to center of the map connector.scrollTo(new ShortPoint2D(header.getWidth() / 2, header.getHeight() / 2), false); } /** * Build the Main Window */ public void buildMapEditingWindow() { JPanel root = new JPanel(); root.setLayout(new BorderLayout(10, 10)); // map display Area area = new Area(); final Region region = new Region(Region.POSITION_CENTER); area.add(region); AreaContainer displayPanel = new AreaContainer(area); displayPanel.setMinimumSize(new Dimension(640, 480)); displayPanel.setFocusable(true); root.add(displayPanel, BorderLayout.CENTER); window = new EditorFrame(root, sidebar) { private static final long serialVersionUID = 1L; @Override protected JComponent createPlayerSelectSelection() { return playerCombobox; } }; registerActions(); window.initMenubarAndToolbar(); initActions(); validator.reValidate(); // window.pack(); window.setSize(1200, 800); window.invalidate(); window.setFilename(getHeader().getName()); // center on screen window.setLocationRelativeTo(null); this.mapContent = new MapContent(new FakeMapGame(map), new SwingSoundPlayer(), ETextDrawPosition.TOP_RIGHT, new MapEditorControls(new CombiningActionFirerer(this))); connector = mapContent.getInterfaceConnector(); region.setContent(mapContent); redrawTimer.schedule(new TimerTask() { @Override public void run() { region.requestRedraw(); } }, 50, 50); connector.addListener(this); window.setVisible(true); displayPanel.requestFocus(); } /** * Quit this editor instance */ private void quit() { redrawTimer.cancel(); validator.dispose(); window.dispose(); } /** * Open an existing file */ private void openExistingFile() { if (!checkSaved()) { // user canceled return; } OpenExistingDialog dlg = new OpenExistingDialog(window); dlg.setVisible(true); if (!dlg.isConfirmed()) { return; } // only one map can be active quit(); // create new control for new map EditorControl control = new EditorControl(); try { control.loadMap(dlg.getSelectedMap()); } catch (MapLoadException e) { ExceptionHandler.displayError(e, "Could not load map"); } } /** * Create a new map */ private void createNewFile() { if (!checkSaved()) { // user canceled return; } NewFileDialog dlg = new NewFileDialog(window); dlg.setVisible(true); if (!dlg.isConfirmed()) { return; } MapFileHeader header = dlg.getHeader(); // only one map can be active quit(); // create new control for new map EditorControl control = new EditorControl(); control.createNewMap(header, dlg.getGroundTypes()); } /** * Check if saved, if not ask user * * @return true to continue, false to cancel */ private boolean checkSaved() { if (!undoRedo.isChangedSinceLastSave()) { return true; } else { int result = JOptionPane.showConfirmDialog(window, EditorLabels.getLabel("ctrl.save-chages"), "JSettler", JOptionPane.YES_NO_CANCEL_OPTION); if (result == JOptionPane.YES_OPTION) { save(); return true; } else if (result == JOptionPane.NO_OPTION) { return true; } // cancel return false; } } /** * Register toolbar / menubar actions */ private void registerActions() { window.registerAction("quit", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (checkSaved()) { quit(); // TODO dispose all window, make all threads deamon, then remove this exit! System.exit(0); } } }); window.registerAction("new", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { createNewFile(); } }); window.registerAction("open", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { openExistingFile(); } }); window.registerAction("zoom-in", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { mapContent.zoomIn(); } }); window.registerAction("zoom-out", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { mapContent.zoomOut(); } }); window.registerAction("zoom100", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { mapContent.zoom100(); } }); window.registerAction("show-resources-always", new AbstractAction() { private static final long serialVersionUID = 1L; { putValue(EditorFrame.DISPLAY_CHECKBOX, true); } @Override public void actionPerformed(ActionEvent e) { Boolean checked = (Boolean) getValue(EditorFrame.CHECKBOX_VALUE); if (checked != null && checked) { showResourcesAlways = true; } else { showResourcesAlways = false; } map.setShowResources(showResourcesAlways | showResourcesBecauseOfTool); } }); window.registerAction("save", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { save(); } }); window.registerAction("save-as", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { String name = JOptionPane.showInputDialog(window, EditorLabels.getLabel("ctrl.save-as-name")); if (name != null) { createNewHeaderWithName(name); save(); window.setFilename(name); } } }); window.registerAction("open-map-folder", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { try { Desktop.getDesktop().open(new File(ResourceManager.getResourcesDirectory(), "maps")); } catch (IOException e1) { ExceptionHandler.displayError(e1, "Could not open map folder"); } } }); window.registerAction("undo", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { undoRedo.undo(); validator.reValidate(); } }); window.registerAction("redo", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { undoRedo.redo(); validator.reValidate(); } }); window.registerAction("show-statistic", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { StatisticsDialog dlg = new StatisticsDialog(window, mapData); dlg.setVisible(true); } }); window.registerAction("show-map-settings", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { editSettings(); } }); final AbstractAction playAction = new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { play(); } }; window.registerAction("play", playAction); validator.addListener(new ValidationResultListener() { @Override public void validationFinished(ValidationListModel list) { playAction.setEnabled(list.size() == 0); } }); window.registerAction("show-tools", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { sidebar.showTools(); } }); ShowErrorsAction showErrorsAction = new ShowErrorsAction(sidebar, true); ShowErrorsAction showWarningsAction = new ShowErrorsAction(sidebar, false); window.registerAction("show-errors", showErrorsAction); window.registerAction("show-warnings", showWarningsAction); validator.addListener(showErrorsAction); validator.addListener(showWarningsAction); // show-warnings GotoNextErrorAction gotoNextErrorAction = new GotoNextErrorAction(this); window.registerAction("goto-error", gotoNextErrorAction); validator.addListener(gotoNextErrorAction); this.autoFixErrorAction = new AutoFixErrorAction(); window.registerAction("auto-fix-error", autoFixErrorAction); validator.addListener(autoFixErrorAction); window.registerAction("locate-player", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { int playerId = getActivePlayer(); scrollTo(mapData.getStartPoint(playerId)); } }); } /** * Set some actions disabled per default, will be enabled when they are available */ private void initActions() { window.enableAction("save", false); window.enableAction("undo", false); window.enableAction("redo", false); } /** * Display the map settings dialog */ protected void editSettings() { SettingsDialog dlg = new SettingsDialog(window, getHeader()) { private static final long serialVersionUID = 1L; @Override public void applyNewHeader(MapFileHeader header) { setHeader(header); mapData.setMaxPlayers(header.getMaxPlayers()); updatePlayerCombobox(); validator.reValidate(); } }; dlg.setVisible(true); } /** * Save current map */ protected void save() { try { MapFileHeader imagedHeader = generateMapHeader(); new LastUsedHandler().saveUsedMapId(imagedHeader.getUniqueId()); mapData.doPreSaveActions(); CommonConstants.USE_SAVEGAME_COMPRESSION = false; MapList.getDefaultList().saveNewMap(imagedHeader, mapData, null); undoRedo.setSaved(); } catch (Throwable e) { ExceptionHandler.displayError(e, "Error saving"); } } /** * Start Another process * * @param args * Arguments * @param name * For Log output and thread ID * @throws IOException */ protected void startProcess(String[] args, final String name) throws IOException { System.out.println("Starting process:"); for (String arg : args) { System.out.print(arg + " "); } System.out.println(); ProcessBuilder builder = new ProcessBuilder(args); builder.redirectErrorStream(true); final Process process = builder.start(); Thread streamReader = new Thread(new Runnable() { @Override public void run() { BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); while (true) { String line; try { line = reader.readLine(); } catch (IOException e) { break; } if (line == null) { break; } System.out.println(name + " " + line); } } }, "ExecThread " + name); streamReader.setDaemon(true); } /** * Play the game in a new process */ protected void play() { try { File temp = File.createTempFile("tmp_map", ""); mapData.doPreSaveActions(); MapList.getDefaultList().saveNewMap(generateMapHeader(), mapData, new FileOutputStream(temp)); String[] args = new String[] { "java", "-classpath", System.getProperty("java.class.path"), SwingManagedJSettlers.class.getName(), "--mapfile=" + temp.getAbsolutePath(), "--control-all", "--activate-all-players" }; startProcess(args, "game"); } catch (IOException e) { ExceptionHandler.displayError(e, "Failed to start game"); } } /** * Set the selected tool * * @param lastPathComponent */ protected void changeTool(Tool lastPathComponent) { tool = lastPathComponent; toolSidebar.updateShapeSettings(tool); showResourcesBecauseOfTool = false; if (tool != null) { // if the resource tool is used they should be displayed showResourcesBecauseOfTool |= tool instanceof ResourceTool; if (tool instanceof PlaceBuildingTool) { PlaceBuildingTool pbt = (PlaceBuildingTool) tool; EBuildingType type = pbt.getType(); // display resources for Mines and Fisher if (type.isMine() || type == EBuildingType.FISHER) { showResourcesBecauseOfTool = true; } } } map.setShowResources(showResourcesAlways | showResourcesBecauseOfTool); } /** * This listener is called from different Thread, all swing calls have to be from event dispatcher thread! */ @Override public void action(final IAction action) { System.out.println("Got action: " + action.getActionType()); if (action.getActionType() == EActionType.SELECT_AREA) { // IMapArea area = ((SelectAreaAction) action).getArea(); } else if (action instanceof DrawLineAction) { if (tool != null && !(tool instanceof SetStartpointTool)) { DrawLineAction lineAction = (DrawLineAction) action; // only getter call, no Swing calls ShapeType shape = toolSidebar.getActiveShape(); tool.apply(mapData, shape, lineAction.getStart(), lineAction.getEnd(), lineAction.getUidy()); validator.reValidate(); } } else if (action instanceof StartDrawingAction) { if (tool != null && !(tool instanceof SetStartpointTool)) { StartDrawingAction lineAction = (StartDrawingAction) action; // only getter call, no Swing calls ShapeType shape = toolSidebar.getActiveShape(); tool.start(mapData, shape, lineAction.getPos()); validator.reValidate(); } } else if (action instanceof EndDrawingAction) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { undoRedo.endUseStep(); validator.reValidate(); } }); } else if (action instanceof AbortDrawingAction) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { MapDataDelta delta = mapData.getUndoDelta(); if (delta != null) { mapData.apply(delta); } mapData.resetUndoDelta(); validator.reValidate(); } }); } else if (action.getActionType() == EActionType.SELECT_POINT) { if (tool != null) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { PointAction lineAction = (PointAction) action; ShapeType shape = toolSidebar.getActiveShape(); tool.start(mapData, shape, lineAction.getPosition()); tool.apply(mapData, shape, lineAction.getPosition(), lineAction.getPosition(), 0); undoRedo.endUseStep(); validator.reValidate(); } }); } } } @Override public void fireAction(IAction action) { action(action); } @Override public int getActivePlayer() { return currentPlayer; } @Override public void scrollTo(ShortPoint2D pos) { if (pos != null) { connector.scrollTo(pos, true); } } }