package com.kartoflane.superluminal2.ui; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import org.jdom2.input.JDOMParseException; import com.kartoflane.superluminal2.Superluminal; import com.kartoflane.superluminal2.components.Hotkey; import com.kartoflane.superluminal2.components.enums.Hotkeys; import com.kartoflane.superluminal2.components.interfaces.Action; import com.kartoflane.superluminal2.components.interfaces.Predicate; import com.kartoflane.superluminal2.core.Database; import com.kartoflane.superluminal2.core.Manager; import com.kartoflane.superluminal2.ftl.ShipMetadata; import com.kartoflane.superluminal2.ftl.ShipObject; import com.kartoflane.superluminal2.mvc.views.Preview; import com.kartoflane.superluminal2.utils.ShipLoadUtils; import com.kartoflane.superluminal2.utils.UIUtils; import com.kartoflane.superluminal2.utils.Utils; public class ShipLoaderDialog { private static final Logger log = LogManager.getLogger(ShipLoaderDialog.class); private static final Predicate<ShipMetadata> defaultFilter = new Predicate<ShipMetadata>() { public boolean accept(ShipMetadata s) { return true; } }; private static ShipLoaderDialog instance = null; private static final int defaultBlueTabWidth = 200; private static final int defaultClassTabWidth = 150; private static final int minTreeWidth = defaultBlueTabWidth + defaultClassTabWidth + 5; private static final int defaultMetadataWidth = 250; private static ShipMetadata selection = null; private HashMap<ShipMetadata, TreeItem> dataTreeMap = new HashMap<ShipMetadata, TreeItem>(); private HashMap<String, TreeItem> blueprintTreeMap = new HashMap<String, TreeItem>(); private Predicate<ShipMetadata> filter = defaultFilter; private boolean sortByBlueprint = true; private Preview preview = null; private Shell shell; private Text txtBlueprint; private Text txtClass; private Text txtName; private Text txtDescription; private TreeItem trtmPlayer; private TreeItem trtmEnemy; private Tree tree; private Button btnLoad; private Canvas canvas; private Button btnCancel; private TreeColumn trclmnBlueprint; private TreeColumn trclmnClass; public ShipLoaderDialog(Shell parent) { if (instance != null) throw new IllegalStateException("Previous instance has not been disposed!"); instance = this; preview = new Preview(); shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL); shell.setText(Superluminal.APP_NAME + " - Ship Loader"); shell.setLayout(new GridLayout(2, false)); SashForm sashForm = new SashForm(shell, SWT.SMOOTH); sashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); tree = new Tree(sashForm, SWT.BORDER | SWT.FULL_SELECTION); tree.setHeaderVisible(true); // remove the horizontal bar so that it doesn't flicker when the tree is resized tree.getHorizontalBar().dispose(); trclmnBlueprint = new TreeColumn(tree, SWT.LEFT); trclmnBlueprint.setWidth(defaultBlueTabWidth); trclmnBlueprint.setText("Blueprint Name"); trclmnBlueprint.setToolTipText("Click to sort by blueprint name."); trclmnClass = new TreeColumn(tree, SWT.RIGHT); trclmnClass.setWidth(defaultClassTabWidth); trclmnClass.setText("Ship Class"); trclmnClass.setToolTipText("Click to sort by class name."); trtmPlayer = new TreeItem(tree, SWT.NONE); trtmPlayer.setText(0, "Player Ships"); trtmPlayer.setText(1, ""); trtmEnemy = new TreeItem(tree, 0); trtmEnemy.setText(0, "Enemy Ships"); trtmEnemy.setText(1, ""); ScrolledComposite scrolledComposite = new ScrolledComposite(sashForm, SWT.BORDER | SWT.V_SCROLL); scrolledComposite.setExpandHorizontal(true); scrolledComposite.setExpandVertical(true); Composite metadataComposite = new Composite(scrolledComposite, SWT.NONE); metadataComposite.setLayout(new GridLayout(2, false)); canvas = new Canvas(metadataComposite, SWT.DOUBLE_BUFFERED); GridData gd_canvas = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1); gd_canvas.heightHint = 100; canvas.setLayoutData(gd_canvas); canvas.addPaintListener(preview); Label lblBlueprint = new Label(metadataComposite, SWT.NONE); lblBlueprint.setText("Blueprint:"); txtBlueprint = new Text(metadataComposite, SWT.BORDER | SWT.READ_ONLY); txtBlueprint.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 1, 1)); Label lblClass = new Label(metadataComposite, SWT.NONE); lblClass.setText("Class:"); txtClass = new Text(metadataComposite, SWT.BORDER | SWT.READ_ONLY); txtClass.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); Label lblName = new Label(metadataComposite, SWT.NONE); lblName.setText("Name:"); txtName = new Text(metadataComposite, SWT.BORDER | SWT.READ_ONLY); txtName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); Label lblDescription = new Label(metadataComposite, SWT.NONE); lblDescription.setLayoutData(new GridData(SWT.LEFT, SWT.BOTTOM, false, false, 2, 1)); lblDescription.setText("Description:"); txtDescription = new Text(metadataComposite, SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL | SWT.MULTI); GridData gd_txtDescription = new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1); gd_txtDescription.heightHint = 100; txtDescription.setLayoutData(gd_txtDescription); scrolledComposite.setContent(metadataComposite); scrolledComposite.setMinSize(metadataComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); sashForm.setWeights(new int[] { minTreeWidth, defaultMetadataWidth }); Composite buttonComposite = new Composite(shell, SWT.NONE); GridLayout gl_buttonComposite = new GridLayout(3, false); gl_buttonComposite.marginWidth = 0; gl_buttonComposite.marginHeight = 0; buttonComposite.setLayout(gl_buttonComposite); buttonComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1)); Button btnSearch = new Button(buttonComposite, SWT.NONE); btnSearch.setText("Search"); GridData gd_btnSearch = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1); gd_btnSearch.widthHint = 80; btnSearch.setLayoutData(gd_btnSearch); btnLoad = new Button(buttonComposite, SWT.NONE); GridData gd_btnLoad = new GridData(SWT.RIGHT, SWT.CENTER, true, false, 1, 1); gd_btnLoad.widthHint = 80; btnLoad.setLayoutData(gd_btnLoad); btnLoad.setText("Load"); btnLoad.setEnabled(false); btnCancel = new Button(buttonComposite, SWT.NONE); GridData gd_btnCancel = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1); gd_btnCancel.widthHint = 80; btnCancel.setLayoutData(gd_btnCancel); btnCancel.setText("Close"); tree.addMouseListener(new MouseAdapter() { @Override public void mouseDoubleClick(MouseEvent e) { if (e.button == 1 && tree.getSelectionCount() != 0) { TreeItem selectedItem = tree.getSelection()[0]; if (selectedItem.getItemCount() == 0 && btnLoad.isEnabled()) btnLoad.notifyListeners(SWT.Selection, null); else if (selectedItem.getBounds().contains(e.x, e.y)) selectedItem.setExpanded(!selectedItem.getExpanded()); } } }); tree.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (tree.getSelectionCount() != 0) { TreeItem selectedItem = tree.getSelection()[0]; selection = (ShipMetadata) selectedItem.getData(); if (selection == null) { btnLoad.setEnabled(false); txtBlueprint.setText(""); txtClass.setText(""); txtName.setText(""); txtDescription.setText(""); preview.setImage(null); canvas.redraw(); } else { btnLoad.setEnabled(true); txtBlueprint.setText(selection.getBlueprintName()); txtClass.setText(selection.getShipClass()); if (selection.isPlayerShip()) { txtName.setText(selection.getShipName()); txtDescription.setText(selection.getShipDescription()); } else { txtName.setText("N/A"); txtDescription.setText("N/A"); } String path = selection.getHullImagePath(); preview.setImage(path == null ? "db:img/nullResource.png" : path); updatePreview(); canvas.redraw(); } } else { btnLoad.setEnabled(false); preview.setImage(null); canvas.redraw(); } } }); trclmnBlueprint.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Sort by blueprint name if (!sortByBlueprint) { sortByBlueprint = true; loadShipList(Database.getInstance().getShipMetadata()); } } }); trclmnClass.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // Sort by class name if (sortByBlueprint) { sortByBlueprint = false; loadShipList(Database.getInstance().getShipMetadata()); } } }); btnSearch.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { ShipSearchDialog ssDialog = new ShipSearchDialog(shell); Predicate<ShipMetadata> resultFilter = ssDialog.open(); if (resultFilter == AbstractSearchDialog.RESULT_DEFAULT) { if (filter == defaultFilter) return; filter = defaultFilter; } else if (resultFilter == AbstractSearchDialog.RESULT_UNCHANGED) { // Do nothing return; } else { filter = resultFilter; } loadShipList(); tree.notifyListeners(SWT.Selection, null); } }); btnLoad.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (tree.getSelectionCount() != 0) { TreeItem selectedItem = tree.getSelection()[0]; ShipMetadata metadata = (ShipMetadata) selectedItem.getData(); try { ShipObject object = ShipLoadUtils.loadShipXML(metadata.getElement()); if (!Manager.allowRoomOverlap && object.hasOverlappingRooms()) { log.info("Ship contains overlapping rooms, but overlap is disabled - forcing room overlap enable."); Manager.allowRoomOverlap = true; } Manager.loadShip(object); if (Manager.closeLoader) { dispose(); } } catch (IllegalArgumentException ex) { handleException(metadata, ex); } catch (JDOMParseException ex) { handleException(metadata, ex); } catch (IOException ex) { handleException(metadata, ex); } } } }); btnCancel.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { dispose(); } }); shell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event e) { btnCancel.notifyListeners(SWT.Selection, null); e.doit = false; } }); canvas.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { updatePreview(); canvas.redraw(); } }); ControlAdapter resizer = new ControlAdapter() { @Override public void controlResized(ControlEvent e) { final int BORDER_OFFSET = tree.getBorderWidth(); if (trclmnBlueprint.getWidth() > tree.getClientArea().width - BORDER_OFFSET) trclmnBlueprint.setWidth(tree.getClientArea().width - BORDER_OFFSET); trclmnClass.setWidth(tree.getClientArea().width - trclmnBlueprint.getWidth() - BORDER_OFFSET); } }; tree.addControlListener(resizer); trclmnBlueprint.addControlListener(resizer); shell.setMinimumSize(minTreeWidth + defaultMetadataWidth, 300); shell.pack(); Point size = shell.getSize(); shell.setSize(size.x + 5, size.y); Point parSize = parent.getSize(); Point parLoc = parent.getLocation(); shell.setLocation(parLoc.x + parSize.x / 3 - size.x / 2, parLoc.y + parSize.y / 3 - size.y / 2); // Register hotkeys Hotkey h = new Hotkey(); h.setOnPress(new Action() { public void execute() { if (tree.getSelectionCount() != 0) { TreeItem selectedItem = tree.getSelection()[0]; if (selectedItem.getItemCount() == 0 && btnLoad.isEnabled()) btnLoad.notifyListeners(SWT.Selection, null); else selectedItem.setExpanded(!selectedItem.getExpanded()); } } }); h.setKey(SWT.CR); Manager.hookHotkey(shell, h); h = new Hotkey(Manager.getHotkey(Hotkeys.SEARCH)); h.addNotifyAction(btnSearch, true); Manager.hookHotkey(shell, h); loadShipList(); } public void loadShipList(HashMap<String, ArrayList<ShipMetadata>> metadataMap) { for (TreeItem it : dataTreeMap.values()) it.dispose(); for (TreeItem it : blueprintTreeMap.values()) it.dispose(); dataTreeMap.clear(); blueprintTreeMap.clear(); Database db = Database.getInstance(); MetadataIterator it = new MetadataIterator(metadataMap, sortByBlueprint); for (it.first(); it.hasNext(); it.next()) { String blueprint = it.current(); boolean isPlayer = db.isPlayerShip(blueprint); TreeItem blueprintItem = new TreeItem(isPlayer ? trtmPlayer : trtmEnemy, SWT.NONE); blueprintItem.setText(0, blueprint); blueprintItem.setText(1, ""); ArrayList<ShipMetadata> dataList = metadataMap.get(blueprint); if (dataList.size() == 1) { ShipMetadata metadata = dataList.get(0); blueprintItem.setData(metadata); blueprintItem.setText(1, metadata.getShipClass()); dataTreeMap.put(metadata, blueprintItem); } else { for (ShipMetadata metadata : dataList) { TreeItem metadataItem = new TreeItem(blueprintItem, SWT.NONE); metadataItem.setText(0, blueprint); metadataItem.setText(1, metadata.getShipClass()); metadataItem.setData(metadata); dataTreeMap.put(metadata, metadataItem); } } blueprintTreeMap.put(blueprint, blueprintItem); } } public void loadShipList() { Database db = Database.getInstance(); HashMap<String, ArrayList<ShipMetadata>> ships = db.getShipMetadata(); if (filter != null && filter != defaultFilter) { String[] set = ships.keySet().toArray(new String[0]); for (String s : set) { ShipMetadata[] mdata = ships.get(s).toArray(new ShipMetadata[0]); for (ShipMetadata md : mdata) { if (!filter.accept(md)) ships.get(s).remove(md); } if (ships.get(s).isEmpty()) ships.remove(s); } } loadShipList(ships); } private void handleException(ShipMetadata metadata, Exception ex) { log.warn("An error has occured while loading " + metadata.getBlueprintName() + ": ", ex); StringBuilder buf = new StringBuilder(); buf.append(metadata.getBlueprintName()); buf.append(" could not be loaded:\n\n"); buf.append(ex.getClass().getSimpleName()); buf.append(": "); buf.append(ex.getMessage()); buf.append("\n\nCheck the log or console for details."); UIUtils.showWarningDialog(shell, null, buf.toString()); } private void updatePreview() { Point iSize = preview.getImageSize(); Point cSize = canvas.getSize(); double ratio = (double) iSize.y / iSize.x; int w = (int) (cSize.y / ratio); int h = (int) (cSize.x * ratio); preview.setSize(Utils.min(w, cSize.x, iSize.x), Utils.min(h, cSize.y, iSize.y)); preview.setLocation(cSize.x / 2, cSize.y / 2); } public void open() { TreeItem item = dataTreeMap.get(selection); if (item != null) { tree.select(item); tree.setTopItem(item); tree.notifyListeners(SWT.Selection, null); } shell.open(); } public boolean isActive() { return !shell.isDisposed() && shell.isVisible(); } public static ShipLoaderDialog getInstance() { return instance; } public void dispose() { Manager.unhookHotkeys(shell); dataTreeMap.clear(); blueprintTreeMap.clear(); preview.dispose(); shell.dispose(); instance = null; } private class MetadataIterator implements Iterator<String> { private final HashMap<String, ArrayList<ShipMetadata>> map; private final MetadataComparator comparator; private String current = null; public MetadataIterator(HashMap<String, ArrayList<ShipMetadata>> map, boolean byBlueprint) { comparator = new MetadataComparator(map, byBlueprint); this.map = map; } private String getSmallestElement() { String result = null; for (String blueprint : map.keySet()) { if (result == null || comparator.compare(blueprint, result) < 0) result = blueprint; } return result; } public void first() { current = getSmallestElement(); } public String current() { return current; } @Override public boolean hasNext() { return !map.isEmpty(); } @Override public String next() { remove(); current = getSmallestElement(); return current; } @Override public void remove() { map.remove(current); } } private class MetadataComparator implements Comparator<String> { private final boolean byBlueprint; private final HashMap<String, ArrayList<ShipMetadata>> map; public MetadataComparator(HashMap<String, ArrayList<ShipMetadata>> map, boolean byBlueprint) { this.byBlueprint = byBlueprint; this.map = map; } @Override public int compare(String o1, String o2) { if (byBlueprint) { // Just compare the two blueprints together for alphanumerical ordering return o1.compareTo(o2); } else { // If there are multiple ships overriding the same blueprint, sorting by class name // becomes tricky. Take class name of the default ship and sort by that. ShipMetadata m1, m2; m1 = map.get(o1).get(0); m2 = map.get(o2).get(0); int result = m1.getShipClass().compareTo(m2.getShipClass()); if (result == 0) // If class names are the same, fall back to sorting by blueprint result = o1.compareTo(o2); return result; } } } }