package com.kartoflane.superluminal2.core;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Text;
import com.kartoflane.superluminal2.Superluminal;
import com.kartoflane.superluminal2.components.Hotkey;
import com.kartoflane.superluminal2.components.KeybindHandler;
import com.kartoflane.superluminal2.components.enums.Hotkeys;
import com.kartoflane.superluminal2.ftl.ShipObject;
import com.kartoflane.superluminal2.mvc.controllers.AbstractController;
import com.kartoflane.superluminal2.mvc.controllers.ShipController;
import com.kartoflane.superluminal2.tools.Tool;
import com.kartoflane.superluminal2.tools.Tool.Tools;
import com.kartoflane.superluminal2.ui.EditorWindow;
import com.kartoflane.superluminal2.ui.OverviewWindow;
import com.kartoflane.superluminal2.ui.ShipContainer;
import com.kartoflane.superluminal2.utils.IOUtils;
import com.kartoflane.superluminal2.utils.UIUtils;
/**
* Manager class to manage the current ship, interface flags, object & tool selection,
* hotkey registration and handling, undo/redo system, and requesting of file handles
* via the editor's protocol system.
*
* @author kartoFlane
*/
public abstract class Manager {
private static final Logger log = LogManager.getLogger(Manager.class);
/**
* A regex pattern which matches the path to the file, along with its extension.
* For example, in 'C:/path/file.ext/inner/path' matches 'C:/path/file.ext'<br>
* <br>
* To be specific, matches all strings that meet the following conditions:<br>
* - Starts with a non-zero number of any characters<br>
* - Those characters then have to be terminated by a period<br>
* - After that, a non-zero number of any characters other than a forward slash
*/
private static final Pattern filePattern = Pattern.compile(".+\\.[^/]+");
private static final HashMap<Hotkeys, Hotkey> hotkeyMap = new HashMap<Hotkeys, Hotkey>();
private static final HashMap<Tools, Tool> toolMap = new HashMap<Tools, Tool>();
// Config variables
public static boolean sidebarOnRightSide = true;
public static boolean rememberGeometry = true;
public static boolean startMaximised = false;
public static boolean closeLoader = false;
public static boolean checkUpdates = true;
public static Point windowSize = null;
public static String resourcePath = "";
public static boolean allowRoomOverlap = false;
public static boolean allowDoorOverlap = false;
public static boolean resetDoorLinksOnMove = false;
public static boolean mouseShipRelative = false;
public static boolean shownSlotWarning = false;
public static boolean shownArtilleryWarning = false;
// Runtime variables
public static boolean leftMouseDown = false;
public static boolean midMouseDown = false;
public static boolean rightMouseDown = false;
private static AbstractController selectedController;
private static ShipContainer currentShip = null;
private static Tools selectedTool = null;
private static KeybindHandler keyHandler = new KeybindHandler();
private static UndoManager undoManager = new UndoManager();
/*
* ================
* Selection system
* ================
*/
/**
* Selects the given box, deselecting the previous one if it was already selected.<br>
*
* @param box
* the box that is to be selected. Can be null to clear selection.
*/
public static void setSelected(AbstractController controller) {
if (controller != null && !controller.isSelectable())
throw new IllegalArgumentException("Controller is not selectable.");
if (selectedController != null)
selectedController.deselect();
selectedController = null;
if (controller != null) {
controller.select();
selectedController = controller;
}
try {
EditorWindow.getInstance().setSidebarContentController(controller);
} catch (UnsupportedOperationException e) {
// Ignore
}
}
/** @return the currently selected controller. */
public static AbstractController getSelected() {
return selectedController;
}
/*
* =====================================
* Ship system - loading/saving/creation
* =====================================
*/
public static void createNewShip(boolean playerShip) {
EditorWindow window = EditorWindow.getInstance();
if (!closeShip())
return;
currentShip = new ShipContainer(window, new ShipObject(playerShip));
currentShip.getShipController().reposition(3 * ShipContainer.CELL_SIZE, 3 * ShipContainer.CELL_SIZE);
window.enableTools(true);
window.enableOptions(true);
window.setVisibilityOptions(true);
// select the manipulation tool by default
selectTool(Tools.IMAGES);
OverviewWindow.staticUpdate();
}
public static void loadShip(ShipObject ship) {
EditorWindow window = EditorWindow.getInstance();
if (!closeShip())
return;
currentShip = new ShipContainer(window, ship);
ShipController sc = currentShip.getShipController();
// Selecting the anchor makes children non-collidable
// Prevents rooms and doors from bugging out when repositioning
sc.select();
sc.reposition(3 * ShipContainer.CELL_SIZE, 3 * ShipContainer.CELL_SIZE);
sc.deselect();
currentShip.updateBoundingArea();
currentShip.updateChildBoundingAreas();
window.enableTools(true);
window.enableOptions(true);
window.setVisibilityOptions(true);
// select the manipulation tool by default
selectTool(Tools.IMAGES);
OverviewWindow.staticUpdate();
}
/**
* Attempts to close the ship, asking user beforehand if the ship is not saved and there are
* edits that can be undone.
*
* @return true if the ship was closed, false otherwise
*/
public static boolean closeShip() {
if (currentShip == null)
return true;
if (undoManager.canUndo() && !currentShip.isSaved()) {
String msg = "Your ship has unsaved changes. Do you wish to save them?";
int response = UIUtils.showYesNoCancelDialog(EditorWindow.getInstance().getShell(),
Superluminal.APP_NAME + " - Save Changes?", msg);
if (response == SWT.YES) {
if (!EditorWindow.getInstance().promptSaveShip(currentShip, false))
return false; // Abort if the user cancelled saving
} else if (response == SWT.NO) {
// Ignore and continue
} else if (response == SWT.CANCEL) {
// Abort
return false;
}
}
closeShipForce();
return true;
}
/**
* Closes the ship without asking the user for confirmation.
*/
public static void closeShipForce() {
EditorWindow window = EditorWindow.getInstance();
setSelected(null);
undoManager.discardAllEdits();
if (currentShip != null)
currentShip.dispose();
currentShip = null;
window.enableTools(false);
window.enableOptions(false);
window.canvasRedraw();
selectTool(null);
OverviewWindow.staticUpdate();
}
/** Returns the currently loaded ship. */
public static ShipContainer getCurrentShip() {
return currentShip;
}
/*
* =============================
* Tool system -- tool selection
* =============================
*/
/**
* Select the given tool item, also triggering the tools' {@link Tool#select() select()} and {@link Tool#deselect() deselect()} methods, as
* needed.
*/
public static void selectTool(Tools tool) {
// Deny trying to select the same tool twice
if (selectedTool != null && selectedTool == tool)
return;
EditorWindow window = EditorWindow.getInstance();
if (selectedTool != null)
toolMap.get(selectedTool).deselect();
selectedTool = tool;
if (tool != null) {
MouseInputDispatcher.getInstance().setCurrentTool(toolMap.get(tool));
window.selectTool(tool);
toolMap.get(tool).select();
} else {
MouseInputDispatcher.getInstance().setCurrentTool(null);
window.selectTool(null);
window.disposeSidebarContent();
}
}
public static Tools getSelectedToolId() {
return selectedTool;
}
public static Tool getSelectedTool() {
return toolMap.get(selectedTool);
}
public static Tool getTool(Tools tool) {
return toolMap.get(tool);
}
public static void putTool(Tools id, Tool tool) {
if (id == null || tool == null)
throw new IllegalArgumentException("Argument must not be null.");
toolMap.put(id, tool);
}
/*
* ==================================================
* Hotkey system -- loading/registration/notification
* ==================================================
*/
public static Hotkey getHotkey(Hotkeys key) {
return hotkeyMap.get(key);
}
public static void putHotkey(Hotkeys key, Hotkey h) {
if (key == null || h == null)
throw new IllegalArgumentException("Argument must not be null.");
hotkeyMap.put(key, h);
}
/** Loads the default hotkey values, which are later overridden by config or the user. */
public static void loadDefaultHotkeys() {
Hotkey hotkey = null;
// Tool hotkeys
getHotkey(Hotkeys.POINTER_TOOL).setKey('q');
getHotkey(Hotkeys.CREATE_TOOL).setKey('w');
getHotkey(Hotkeys.IMAGES_TOOL).setKey('e');
getHotkey(Hotkeys.PROPERTIES_TOOL).setKey('r');
getHotkey(Hotkeys.ROOM_TOOL).setKey('a');
getHotkey(Hotkeys.DOOR_TOOL).setKey('s');
getHotkey(Hotkeys.MOUNT_TOOL).setKey('d');
getHotkey(Hotkeys.STATION_TOOL).setKey('f');
getHotkey(Hotkeys.OVERVIEW_TOOL).setKey('o');
// Command hotkeys
getHotkey(Hotkeys.PIN).setKey(' ');
hotkey = getHotkey(Hotkeys.SEARCH);
hotkey.setKey('f');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.DELETE);
hotkey.setKey('d');
hotkey.setShift(true);
hotkey = getHotkey(Hotkeys.UNDO);
hotkey.setKey('z');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.REDO);
hotkey.setKey('y');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.NEW_SHIP);
hotkey.setKey('n');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.LOAD_SHIP);
hotkey.setKey('l');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.SAVE_SHIP);
hotkey.setKey('s');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.CLOSE_SHIP);
hotkey.setKey('w');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.MANAGE_MOD);
hotkey.setKey('m');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.SETTINGS);
hotkey.setKey('o');
hotkey.setCtrl(true);
hotkey = getHotkey(Hotkeys.CLOAK);
hotkey.setKey('c');
hotkey.setShift(true);
getHotkey(Hotkeys.OPEN_ZOOM).setEnabled(false);
getHotkey(Hotkeys.ANIMATE).setEnabled(false);
getHotkey(Hotkeys.LOAD_LEGACY).setEnabled(false);
getHotkey(Hotkeys.SAVE_SHIP_AS).setEnabled(false);
// View hotkeys
getHotkey(Hotkeys.TOGGLE_GRID).setKey('g');
getHotkey(Hotkeys.TOGGLE_HANGAR).setKey('h');
getHotkey(Hotkeys.SHOW_ANCHOR).setKey('1');
getHotkey(Hotkeys.SHOW_MOUNTS).setKey('2');
getHotkey(Hotkeys.SHOW_ROOMS).setKey('3');
getHotkey(Hotkeys.SHOW_DOORS).setKey('4');
getHotkey(Hotkeys.SHOW_STATIONS).setKey('5');
getHotkey(Hotkeys.SHOW_HULL).setKey('6');
getHotkey(Hotkeys.SHOW_FLOOR).setKey('7');
getHotkey(Hotkeys.SHOW_SHIELD).setKey('8');
getHotkey(Hotkeys.SHOW_GIBS).setKey('9');
}
protected static void notifyKeyPressed(KeyEvent e) {
Display display = UIUtils.getDisplay();
// Don't execute the hotkey action if an editable widget has focus
Control c = display.getFocusControl();
boolean execute = (c != null && !(c.isEnabled() && (c instanceof Spinner ||
(c instanceof Text && ((Text) c).getEditable())))) ||
e.keyCode == SWT.CR || e.keyCode == SWT.LF || e.keyCode == SWT.KEYPAD_CR;
if (execute) {
Shell shell = c.getShell();
if (shell != null) {
// Consume the event if the key event triggered a hotkey
e.doit = !keyHandler.notifyPressed(shell, checkShift(e), checkCtrl(e),
checkAlt(e), checkCommand(e), e.keyCode);
}
}
}
protected static void notifyKeyReleased(KeyEvent e) {
Display display = UIUtils.getDisplay();
// Don't execute the hotkey action if an editable widget has focus
Control c = display.getFocusControl();
boolean execute = (c != null && !(c.isEnabled() && (c instanceof Spinner ||
(c instanceof Text && ((Text) c).getEditable())))) ||
e.keyCode == SWT.CR || e.keyCode == SWT.LF || e.keyCode == SWT.KEYPAD_CR;
if (execute) {
Shell shell = c.getShell();
if (shell != null) {
// Consume the event if the key event triggered a hotkey
e.doit = !keyHandler.notifyReleased(shell, checkShift(e), checkCtrl(e),
checkAlt(e), checkCommand(e), e.keyCode);
}
}
}
private static boolean checkShift(KeyEvent e) {
return (e.stateMask & SWT.SHIFT) != 0 || e.keyCode == SWT.SHIFT;
}
private static boolean checkCtrl(KeyEvent e) {
return (e.stateMask & SWT.CTRL) != 0 || e.keyCode == SWT.CTRL;
}
private static boolean checkAlt(KeyEvent e) {
return (e.stateMask & SWT.ALT) != 0 || e.keyCode == SWT.ALT;
}
private static boolean checkCommand(KeyEvent e) {
return (e.stateMask & SWT.COMMAND) != 0 || e.keyCode == SWT.COMMAND;
}
public static void hookHotkey(Shell shell, Hotkey hotkey) {
keyHandler.hook(shell, hotkey);
}
public static void unhookHotkey(Shell shell, Hotkey hotkey) {
keyHandler.unhook(shell, hotkey);
}
public static void unhookHotkeys(Shell shell) {
keyHandler.unhook(shell);
}
/*
* ================
* Undo/Redo system
* ================
*/
/**
* Posts the edit passed in argument to the UndoManager.<br>
* <br>
* This method should not be called directly. Use {@link ShipContainer#postEdit(AbstractUndoableEdit)} instead.
*
* @param aue
* the undoable edit to be posted. Must not be null.
*/
public static void postEdit(AbstractUndoableEdit aue) {
if (aue == null)
throw new IllegalArgumentException("Argument must not be null.");
undoManager.addEdit(aue);
EditorWindow.getInstance().updateUndoButtons();
}
public static void undo() {
try {
undoManager.undo();
EditorWindow.getInstance().updateUndoButtons();
} catch (CannotUndoException e) {
log.trace("Cannot undo:", e);
}
}
public static boolean canUndo() {
return currentShip != null && undoManager.canUndo();
}
public static String getUndoPresentationName() {
return undoManager.getUndoPresentationName();
}
public static void redo() {
try {
undoManager.redo();
EditorWindow.getInstance().updateUndoButtons();
} catch (CannotRedoException e) {
log.trace("Cannot redo:", e);
}
}
public static boolean canRedo() {
return currentShip != null && undoManager.canRedo();
}
public static String getRedoPresentationName() {
return undoManager.getRedoPresentationName();
}
/*
* ==============
* Miscellanneous
* ==============
*/
/**
* Request an InputStream for the given path.<br>
* <br>
* All paths must have a protocol declared at their beginning, like so:
*
* <pre>
* <tt>file:C:/example/absolute/path.txt</tt>
* </pre>
*
* If a path is missing its protocol, or if it is mistyped, the image will not be loaded.<br>
* Protocols that this method recognizes:
*
* <pre>
* <tt>file: - for use when the resource is located in the OS' filesystem,
* eg. an absolute or relative path
* cpath: - for use when the resource is located inside the jar
* eg. cpath:/assets/image.png
* db: - for use when the resource is loaded in the database
* eg. db:img/ship/kestral_base.png</tt>
* zip: - for use when the resource is located inside an unloaded zip archive
* eg. zip:/path/to/file.zip/inner/path/image.png
* </pre>
*
* @param path
* path to the requested file, preceded by protocol
* @return input stream for the given file, or null if not found
* @throws FileNotFoundException
* when there was no file under the specified path
* @throws IllegalArgumentException
* when the path is null, is wrongly formatted, is missing protocol or uses an
* unknown one, or when zip file path has no inner path
*/
public static InputStream getInputStream(String path) throws FileNotFoundException, IllegalArgumentException {
if (path == null)
throw new IllegalArgumentException("Path must not be null.");
InputStream result = null;
String protocol = IOUtils.getProtocol(path);
String loadPath = IOUtils.trimProtocol(path);
try {
// Employ "protocols" to spare the Cache from having to guess where the file is located
if (protocol.equals("db:")) {
// Refers to file in database
result = Database.getInstance().getInputStream(loadPath);
} else if (protocol.equals("cpath:")) {
// Refers to file in classpath
result = Manager.class.getResourceAsStream(loadPath);
} else if (protocol.equals("file:")) {
// Refers to file in the OS' filesystem
result = new FileInputStream(new File(loadPath));
} else if (protocol.equals("zip:")) {
// Refers to file in a zip archive
Matcher m = filePattern.matcher(loadPath);
if (m.find()) {
String zipPath = m.group(1);
String innerPath = loadPath.replace(zipPath + "/", "");
if (innerPath == null || innerPath.equals(""))
throw new IllegalArgumentException("Path doesn't have an inner path: " + path);
ZipFile zf = null;
try {
zf = new ZipFile(zipPath);
ZipEntry ze = zf.getEntry(innerPath);
if (ze == null)
throw new IllegalArgumentException(String.format("Inner path '%s' was not found in archive '%s'", innerPath, zipPath));
// Closing the ZipFile also closes all streams that it opened...
// Copy the stream so that it's possible to access the file
// without having to keep the archive open
result = IOUtils.cloneStream(zf.getInputStream(ze));
} catch (ZipException e) {
log.warn(String.format("File is not a zip archive: '%s'", zipPath));
} finally {
if (zf != null)
zf.close();
}
} else {
log.warn(String.format("Path was wrongly formatted: '%s'", loadPath));
}
} else {
throw new IllegalArgumentException(String.format("Path uses unknown protocol, or doesn't have it: '%s'", path));
}
} catch (FileNotFoundException e) {
log.warn(String.format("%s - resource could not be found.", path));
} catch (IOException e) {
log.error(String.format("An error has occured while getting input stream for %s: ", loadPath), e);
}
if (result == null)
throw new FileNotFoundException("Could not find file: " + path);
return result;
}
}