/* * Copyright (c) 2016 acmi * * 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 acmi.l2.clientmod.xdat; import acmi.l2.clientmod.crypt.L2Crypt; import acmi.l2.clientmod.l2resources.L2Context; import acmi.l2.clientmod.l2resources.L2Resources; import acmi.l2.clientmod.l2resources.Sysstr; import acmi.l2.clientmod.l2resources.Tex; import acmi.l2.clientmod.unreal.Environment; import acmi.l2.clientmod.util.*; import acmi.l2.clientmod.xdat.propertyeditor.*; import groovy.lang.GroovyClassLoader; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.stage.StageStyle; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.input.CountingInputStream; import org.controlsfx.control.PropertySheet; import org.controlsfx.control.textfield.TextFields; import org.controlsfx.property.editor.PropertyEditor; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.net.URL; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; public class Controller implements Initializable { private static final Logger log = Logger.getLogger(Controller.class.getName()); private static final Map<String, String> UI_NODE_ICONS = new HashMap<>(); private static final String UI_NODE_ICON_DEFAULT = "MissingIcon.png"; static { UI_NODE_ICONS.put("BarCtrl", "ToggleButton.png"); UI_NODE_ICONS.put("Button", "Button.png"); UI_NODE_ICONS.put("CharacterViewportWindow", "MissingIcon.png"); UI_NODE_ICONS.put("ChatWindow", "MissingIcon.png"); UI_NODE_ICONS.put("CheckBox", "CheckBox.png"); UI_NODE_ICONS.put("ComboBox", "ComboBox.png"); UI_NODE_ICONS.put("DrawPanel", "Canvas.png"); UI_NODE_ICONS.put("EditBox", "TextField.png"); UI_NODE_ICONS.put("EffectButton", "Button.png"); UI_NODE_ICONS.put("FishViewportWindow", "MissingIcon.png"); UI_NODE_ICONS.put("FlashCtrl", "MissingIcon.png"); UI_NODE_ICONS.put("HtmlCtrl", "HTMLEditor.png"); UI_NODE_ICONS.put("InvenWeight", "ToggleButton.png"); UI_NODE_ICONS.put("ItemWindow", "GridPane.png"); UI_NODE_ICONS.put("ListBox", "MissingIcon.png"); UI_NODE_ICONS.put("ListCtrl", "ListView.png"); UI_NODE_ICONS.put("MinimapCtrl", "MissingIcon.png"); UI_NODE_ICONS.put("MultiEdit", "MissingIcon.png"); UI_NODE_ICONS.put("MultiSellItemInfo", "MissingIcon.png"); UI_NODE_ICONS.put("MultiSellNeededItem", "MissingIcon.png"); UI_NODE_ICONS.put("NameCtrl", "MissingIcon.png"); UI_NODE_ICONS.put("Progress", "ProgressBar.png"); UI_NODE_ICONS.put("PropertyController", "MissingIcon.png"); UI_NODE_ICONS.put("Radar", "MissingIcon.png"); UI_NODE_ICONS.put("RadarMapCtrl", "MissingIcon.png"); UI_NODE_ICONS.put("RadioButton", "RadioButton.png"); UI_NODE_ICONS.put("ScrollArea", "ScrollPane.png"); UI_NODE_ICONS.put("ShortcutItemWindow", "MissingIcon.png"); UI_NODE_ICONS.put("SliderCtrl", "Slider-h.png"); UI_NODE_ICONS.put("StatusBar", "ToggleButton.png"); UI_NODE_ICONS.put("StatusIconCtrl", "MissingIcon.png"); UI_NODE_ICONS.put("Tab", "TabPane.png"); UI_NODE_ICONS.put("TextBox", "Label.png"); UI_NODE_ICONS.put("TextListBox", "Label.png"); UI_NODE_ICONS.put("Texture", "Graphic.png"); UI_NODE_ICONS.put("TreeCtrl", "TreeView.png"); UI_NODE_ICONS.put("WebBrowserCtrl", "WebView.png"); UI_NODE_ICONS.put("Window", "TitledPane.png"); } private XdatEditor editor; private ResourceBundle interfaceResources; @FXML private MenuItem open; @FXML private MenuItem save; @FXML private MenuItem saveAs; @FXML private Menu versionMenu; private ToggleGroup version = new ToggleGroup(); @FXML private TabPane tabs; @FXML private ProgressBar progressBar; private ObjectProperty<File> initialDirectory = new SimpleObjectProperty<>(this, "initialDirectory", new File(XdatEditor.getPrefs().get("initialDirectory", System.getProperty("user.dir")))); private ObjectProperty<File> xdatFile = new SimpleObjectProperty<>(this, "xdatFile"); private ObjectProperty<Environment> environment = new SimpleObjectProperty<>(this, "environment"); private ObjectProperty<L2Resources> l2resources = new SimpleObjectProperty<>(this, "l2resources"); private List<InvalidationListener> xdatListeners = new ArrayList<>(); public Controller(XdatEditor editor) { this.editor = editor; environment.bind(Bindings.createObjectBinding(() -> { if (xdatFile.getValue() == null) return null; return Environment.fromIni(new File(xdatFile.getValue().getParentFile(), "L2.ini")); }, xdatFile)); l2resources.bind(Bindings.createObjectBinding(() -> { if (environment.getValue() == null) return null; return new L2Resources(environment.get()); }, environment)); } @Override public void initialize(URL location, ResourceBundle resources) { interfaceResources = resources; Node scriptingTab = loadScriptTabContent(); initialDirectory.addListener((observable, oldVal, newVal) -> { if (newVal != null) XdatEditor.getPrefs().put("initialDirectory", newVal.getPath()); }); editor.xdatClassProperty().addListener((ob, ov, nv) -> { log.log(Level.INFO, String.format("XDAT class selected: %s", nv.getName())); tabs.getTabs().clear(); for (Iterator<InvalidationListener> it = xdatListeners.iterator(); it.hasNext(); ) { editor.xdatObjectProperty().removeListener(it.next()); it.remove(); } editor.setXdatObject(null); if (scriptingTab != null) { Tab tab = new Tab("script console"); tab.setContent(scriptingTab); tabs.getTabs().add(tab); } Arrays.stream(nv.getDeclaredFields()) .filter(field -> List.class.isAssignableFrom(field.getType())) .forEach(field -> { field.setAccessible(true); tabs.getTabs().add(createTab(field)); }); }); progressBar.visibleProperty().bind(editor.workingProperty()); open.disableProperty().bind(Bindings.isNull(editor.xdatClassProperty())); BooleanBinding nullXdatObject = Bindings.isNull(editor.xdatObjectProperty()); tabs.disableProperty().bind(nullXdatObject); save.disableProperty().bind(nullXdatObject); saveAs.disableProperty().bind(nullXdatObject); xdatFile.addListener((observable, oldValue, newValue) -> { if (newValue == null) return; Collection<File> files = FileUtils.listFiles(newValue.getParentFile(), new WildcardFileFilter("SysString-*.dat"), null); if (!files.isEmpty()) { File file = files.iterator().next(); log.info("sysstring file: " + file); try (InputStream is = L2Crypt.decrypt(new FileInputStream(file), file.getName())) { SysstringPropertyEditor.strings.clear(); int count = IOUtil.readInt(is); for (int i = 0; i < count; i++) { SysstringPropertyEditor.strings.put(IOUtil.readInt(is), IOUtil.readString(is)); } } catch (Exception ignore) { } } File file = new File(newValue.getParentFile(), "L2.ini"); try { TexturePropertyEditor.environment = Environment.fromIni(file); TexturePropertyEditor.environment.getPaths().forEach(s -> log.info("environment path: " + s)); } catch (Exception ignore) { } }); } public void registerVersion(String name, String xdatClass) { RadioMenuItem menuItem = new RadioMenuItem(name); menuItem.setMnemonicParsing(false); menuItem.selectedProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { editor.execute(() -> { Class<? extends IOEntity> clazz = Class.forName(xdatClass, true, new GroovyClassLoader(getClass().getClassLoader())).asSubclass(IOEntity.class); Platform.runLater(() -> editor.setXdatClass(clazz)); return null; }, e -> { String msg = String.format("%s: XDAT class load error", name); log.log(Level.WARNING, msg, e); Platform.runLater(() -> { version.getToggles().remove(menuItem); versionMenu.getItems().remove(menuItem); Dialogs.showException(Alert.AlertType.ERROR, msg, e.getMessage(), e); }); }); } }); version.getToggles().add(menuItem); versionMenu.getItems().add(menuItem); } private Node loadScriptTabContent() { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("scripting/main.fxml")); loader.setClassLoader(getClass().getClassLoader()); loader.setControllerFactory(param -> new acmi.l2.clientmod.xdat.scripting.Controller(editor)); return wrap(loader.load()); } catch (IOException e) { log.log(Level.WARNING, "Couldn't load script console", e); } return null; } private static AnchorPane wrap(Node node) { AnchorPane anchorPane = new AnchorPane(node); AnchorPane.setTopAnchor(node, 0.0); AnchorPane.setLeftAnchor(node, 0.0); AnchorPane.setRightAnchor(node, 0.0); AnchorPane.setBottomAnchor(node, 0.0); return anchorPane; } private Tab createTab(Field listField) { Tab tab = new Tab(listField.getName()); SplitPane pane = new SplitPane(); TextField filter = TextFields.createClearableTextField(); VBox.setMargin(filter, new Insets(2)); TreeView<Object> elements = createTreeView(listField, filter.textProperty()); VBox.setVgrow(elements, Priority.ALWAYS); PropertySheet properties = createPropertySheet(elements); pane.getItems().addAll(new VBox(filter, elements), properties); pane.setDividerPositions(0.3); tab.setContent(wrap(pane)); return tab; } private TreeView<Object> createTreeView(Field listField, ObservableValue<String> filter) { TreeView<Object> elements = new TreeView<>(); elements.setCellFactory(param -> new TreeCell<Object>() { @Override protected void updateItem(Object item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); setGraphic(null); } else { setText(item.toString()); if (UIEntity.class.isAssignableFrom(item.getClass())) { try (InputStream is = getClass().getResourceAsStream("/acmi/l2/clientmod/xdat/nodeicons/" + UI_NODE_ICONS.getOrDefault(item.getClass().getSimpleName(), UI_NODE_ICON_DEFAULT))) { setGraphic(new ImageView(new Image(is))); } catch (IOException ignore) {} } } } }); elements.setShowRoot(false); elements.setContextMenu(createContextMenu(elements)); InvalidationListener treeInvalidation = (observable) -> buildTree(editor.xdatObjectProperty().get(), listField, elements, filter.getValue()); editor.xdatObjectProperty().addListener(treeInvalidation); xdatListeners.add(treeInvalidation); filter.addListener(treeInvalidation); return elements; } private static void buildTree(IOEntity entity, Field listField, TreeView<Object> elements, String nameFilter) { elements.setRoot(null); if (entity == null) return; try { @SuppressWarnings("unchecked") List<IOEntity> list = (List<IOEntity>) listField.get(entity); if (!listField.isAnnotationPresent(Type.class)) { String msg = String.format("XDAT.%s: @Type not defined", listField.getName()); log.log(Level.WARNING, msg); Dialogs.showException(Alert.AlertType.ERROR, "ReflectiveOperationException", msg, null); } else { Class<? extends IOEntity> type = listField.getAnnotation(Type.class).value().asSubclass(IOEntity.class); TreeItem<Object> rootItem = new TreeItem<>(new ListHolder(entity, list, listField.getName(), type)); elements.setRoot(rootItem); rootItem.getChildren().addAll( list.stream() .map(Controller::createTreeItem) .filter(treeItem -> checkTreeNode(treeItem, nameFilter)) .collect(Collectors.toList())); } } catch (IllegalAccessException e) { String msg = String.format("%s.%s is not accessible", listField.getDeclaringClass().getSimpleName(), listField.getName()); log.log(Level.WARNING, msg, e); Dialogs.showException(Alert.AlertType.ERROR, "ReflectiveOperationException", msg, e); } } private static boolean checkTreeNode(TreeItem<Object> treeItem, String nameFilter) { if (checkName(Objects.toString(treeItem.getValue()), nameFilter)) return true; for (TreeItem<Object> childItem : treeItem.getChildren()) if (checkTreeNode(childItem, nameFilter)) return true; return false; } private static boolean checkName(String s, String nameFilter) { return s.toLowerCase().contains(nameFilter.toLowerCase()); } private ContextMenu createContextMenu(TreeView<Object> elements) { ContextMenu contextMenu = new ContextMenu(); InvalidationListener il = observable1 -> updateContextMenu(contextMenu, elements); elements.rootProperty().addListener(il); elements.getSelectionModel().selectedItemProperty().addListener(il); return contextMenu; } private void updateContextMenu(ContextMenu contextMenu, TreeView<Object> elements) { contextMenu.getItems().clear(); TreeItem<Object> root = elements.getRoot(); TreeItem<Object> selected = elements.getSelectionModel().getSelectedItem(); if (selected == null) { if (root != null) contextMenu.getItems().add(createAddMenu("Add ..", elements, root)); } else { Object value = selected.getValue(); if (value instanceof ListHolder) { contextMenu.getItems().add(createAddMenu("Add ..", elements, selected)); } else if (selected.getParent() != null && selected.getParent().getValue() instanceof ListHolder) { MenuItem add = createAddMenu("Add to parent ..", elements, selected.getParent()); MenuItem delete = new MenuItem("Delete"); delete.setOnAction(event -> { ListHolder parent = (ListHolder) selected.getParent().getValue(); @SuppressWarnings("SuspiciousMethodCalls") int index = parent.list.indexOf(value); if (value instanceof Named) { editor.getHistory().valueRemoved(treeItemToScriptString(selected.getParent()), ((Named) value).getName()); } else { editor.getHistory().valueRemoved(treeItemToScriptString(selected.getParent()), index); } parent.list.remove(index); selected.getParent().getChildren().remove(selected); elements.getSelectionModel().selectPrevious(); elements.getSelectionModel().selectNext(); }); contextMenu.getItems().addAll(add, delete); } if (value instanceof ComponentFactory) { MenuItem view = new MenuItem("View"); view.setOnAction(event -> { if (value instanceof L2Context) ((L2Context) value).setResources(l2resources.getValue()); Stage stage = new Stage(); stage.setTitle(value.toString()); Scene scene = new Scene(((ComponentFactory) value).getComponent()); scene.getStylesheets().add(getClass().getResource("l2.css").toExternalForm()); stage.setScene(scene); stage.show(); }); contextMenu.getItems().add(view); } } } private MenuItem createAddMenu(String name, TreeView<Object> elements, TreeItem<Object> selected) { ListHolder listHolder = (ListHolder) selected.getValue(); MenuItem add = new MenuItem(name); add.setOnAction(event -> { Stream<ClassHolder> st = SubclassManager.getInstance() .getClassWithAllSubclasses(listHolder.type) .stream() .map(ClassHolder::new); List<ClassHolder> list = st .collect(Collectors.toList()); Optional<ClassHolder> choice; if (list.size() == 1) { choice = Optional.of(list.get(0)); } else { ChoiceDialog<ClassHolder> cd = new ChoiceDialog<>(list.get(0), list); cd.setTitle("Select class"); cd.setHeaderText(null); choice = cd.showAndWait(); } choice.ifPresent(toCreate -> { try { IOEntity obj = toCreate.clazz.newInstance(); listHolder.list.add(obj); TreeItem<Object> treeItem = createTreeItem(obj); selected.getChildren().add(treeItem); elements.getSelectionModel().select(treeItem); elements.scrollTo(elements.getSelectionModel().getSelectedIndex()); editor.getHistory().valueCreated(treeItemToScriptString(selected), toCreate.clazz); } catch (ReflectiveOperationException e) { String msg = String.format("Couldn't instantiate %s", toCreate.clazz.getName()); log.log(Level.WARNING, msg, e); Dialogs.showException(Alert.AlertType.ERROR, "ReflectiveOperationException", msg, e); } }); }); return add; } private static TreeItem<Object> createTreeItem(IOEntity o) { TreeItem<Object> item = new TreeItem<>(o); List<Field> fields = new ArrayList<>(); Class<?> clazz = o.getClass(); while (clazz != Object.class) { Arrays.stream(clazz.getDeclaredFields()) .filter(field -> !field.isSynthetic()) .filter(field -> List.class.isAssignableFrom(field.getType()) || IOEntity.class.isAssignableFrom(field.getType())) .forEach(fields::add); clazz = clazz.getSuperclass(); } fields.forEach(field -> { field.setAccessible(true); Optional<Object> obj = Optional.empty(); try { obj = Optional.ofNullable(field.get(o)); } catch (IllegalAccessException e) { String msg = String.format("%s.%s is not accessible", o.getClass(), field.getName()); log.log(Level.WARNING, msg, e); Dialogs.showException(Alert.AlertType.ERROR, "ReflectiveOperationException", msg, e); } obj.ifPresent(val -> { if (List.class.isAssignableFrom(field.getType())) { if (!field.isAnnotationPresent(Type.class)) { String msg = String.format("%s.%s: @Type not defined", o.getClass().getName(), field.getName()); log.log(Level.WARNING, msg); Dialogs.showException(Alert.AlertType.ERROR, "ReflectiveOperationException", msg, null); } else { @SuppressWarnings("unchecked") List<IOEntity> list = (List<IOEntity>) val; Class<? extends IOEntity> type = field.getAnnotation(Type.class).value().asSubclass(IOEntity.class); TreeItem<Object> listItem = new TreeItem<>(new ListHolder(o, list, field.getName(), type)); item.getChildren().add(listItem); listItem.getChildren().addAll(list.stream() .map(Controller::createTreeItem) .collect(Collectors.toList())); } } else if (IOEntity.class.isAssignableFrom(field.getType())) { IOEntity ioEntity = (IOEntity) val; item.getChildren().add(createTreeItem(ioEntity)); } }); }); return item; } private static Map<Class, List<PropertySheetItem>> map = new HashMap<>(); private PropertySheet createPropertySheet(TreeView<Object> elements) { PropertySheet properties = new PropertySheet(); properties.setSkin(new PropertySheetSkin(properties)); elements.getSelectionModel().selectedItemProperty().addListener((selected, oldValue, newSelection) -> { properties.getItems().clear(); if (newSelection == null) return; Object obj = newSelection.getValue(); if (obj instanceof ListHolder) return; if (!map.containsKey(obj.getClass())) { map.put(obj.getClass(), loadProperties(obj)); } List<PropertySheetItem> props = map.get(obj.getClass()); props.forEach(property -> { property.setObject(obj); ChangeListener<Object> addToHistory = (observable1, oldValue1, newValue1) -> { String objName = treeItemToScriptString(newSelection); String propName = ((PropertySheetItem)observable1).getName(); if ("name".equals(propName) || "wnd".equals(propName)){ StringBuilder b = new StringBuilder(objName); String nv = String.valueOf(newValue1); int ind = objName.lastIndexOf(nv); b.replace(ind, ind + nv.length(), String.valueOf(oldValue1) ); objName = b.toString(); } editor.getHistory().valueChanged(objName, property.getName(), newValue1, property.hashCode()); }; property.addListener(addToHistory); selected.addListener(new InvalidationListener() { @Override public void invalidated(Observable observable) { property.removeListener(addToHistory); observable.removeListener(this); } }); }); properties.getItems().setAll(props); }); return properties; } private static List<PropertySheetItem> loadProperties(Object obj) { Class<?> objClass = obj.getClass(); List<PropertySheetItem> list = new ArrayList<>(); while (objClass != Object.class) { for (Field field : objClass.getDeclaredFields()) { if (field.isSynthetic() || Modifier.isStatic(field.getModifiers())) continue; if (Collection.class.isAssignableFrom(field.getType())) continue; if (field.isAnnotationPresent(Hide.class)) continue; String description = ""; if (field.isAnnotationPresent(Description.class)) description = field.getAnnotation(Description.class).value(); Class<? extends PropertyEditor<?>> propertyEditorClass = null; if (field.getType() == Boolean.class || field.getType() == Boolean.TYPE) { propertyEditorClass = BooleanPropertyEditor.class; } else if (field.isAnnotationPresent(Tex.class)) { propertyEditorClass = TexturePropertyEditor.class; } else if (field.isAnnotationPresent(Sysstr.class)) { propertyEditorClass = SysstringPropertyEditor.class; } field.setAccessible(true); PropertySheetItem property = new FieldProperty(field, objClass.getSimpleName(), description, propertyEditorClass); list.add(property); } objClass = objClass.getSuperclass(); } return list; } private String treeItemToScriptString(TreeItem item) { List<TreeItem> list = new ArrayList<>(); do { list.add(item); } while ((item = item.getParent()) != null); Collections.reverse(list); StringBuilder sb = new StringBuilder("xdat"); for (int i = 0; i < list.size(); i++) { Object value = list.get(i).getValue(); if (value instanceof ListHolder) { ListHolder holder = (ListHolder) list.get(i).getValue(); sb.append('.').append(holder.name); if (i + 1 < list.size()) { sb.append('['); Object obj = list.get(++i).getValue(); if (obj instanceof Named) { sb.append('"').append(((Named) obj).getName()).append('"'); } else { //noinspection SuspiciousMethodCalls sb.append(holder.list.indexOf(obj)); } sb.append(']'); } } } return sb.toString(); } @FXML private void open() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Open interface.xdat"); fileChooser.getExtensionFilters().addAll( new FileChooser.ExtensionFilter("XDAT (*.xdat)", "*.xdat"), new FileChooser.ExtensionFilter("All files", "*.*")); if (initialDirectory.getValue() != null && initialDirectory.getValue().exists() && initialDirectory.getValue().isDirectory()) fileChooser.setInitialDirectory(initialDirectory.getValue()); File selected = fileChooser.showOpenDialog(editor.getStage()); if (selected == null) return; xdatFile.setValue(selected); initialDirectory.setValue(selected.getParentFile()); try (DataInputStream dis = new DataInputStream(new FileInputStream(selected))) { int i = Integer.reverseBytes(dis.readInt()); if (i < 0 || i > 0xFFFF) { throw new IOException("File seems to be encrypted."); } } catch (IOException e) { Dialogs.showException(Alert.AlertType.ERROR, "Read error", e.getMessage(), e); return; } try { IOEntity xdat = editor.getXdatClass().getConstructor().newInstance(); editor.execute(() -> { CountingInputStream cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(selected))); try (InputStream is = cis) { xdat.read(is); Platform.runLater(() -> editor.setXdatObject(xdat)); } catch (Throwable e) { String msg = String.format("Read error before offset 0x%x", cis.getCount()); log.log(Level.WARNING, msg, e); throw new IOException(msg, e); } return null; }, e -> Dialogs.showException(Alert.AlertType.ERROR, "Read error", "Try to choose another version", e)); } catch (ReflectiveOperationException e) { String msg = "XDAT class should have empty public constructor"; log.log(Level.WARNING, msg, e); Dialogs.showException(Alert.AlertType.ERROR, "ReflectiveOperationException", msg, e); } } @FXML private void save() { if (xdatFile.getValue() == null) return; editor.execute(() -> { try (OutputStream os = new BufferedOutputStream(new FileOutputStream(xdatFile.getValue()))) { editor.getXdatObject().write(os); } return null; }, e -> { String msg = "Write error"; log.log(Level.WARNING, msg, e); Dialogs.showException(Alert.AlertType.ERROR, msg, e.getMessage(), e); }); } @FXML private void saveAs() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Save"); fileChooser.getExtensionFilters().addAll( new FileChooser.ExtensionFilter("XDAT (*.xdat)", "*.xdat"), new FileChooser.ExtensionFilter("All files", "*.*")); fileChooser.setInitialFileName(xdatFile.getValue().getName()); if (initialDirectory.getValue() != null && initialDirectory.getValue().exists() && initialDirectory.getValue().isDirectory()) fileChooser.setInitialDirectory(initialDirectory.getValue()); File file = fileChooser.showSaveDialog(editor.getStage()); if (file == null) return; this.xdatFile.setValue(file); initialDirectory.setValue(file.getParentFile()); save(); } @FXML private void exit() { Platform.exit(); } @FXML private void about() { Dialog dialog = new Dialog(); dialog.initStyle(StageStyle.UTILITY); dialog.setTitle(interfaceResources.getString("about")); Label name = new Label("XDAT Editor"); Label version = new Label("Version: " + editor.getApplicationVersion()); Label jre = new Label("JRE: " + System.getProperty("java.version")); Label jvm = new Label("JVM: " + System.getProperty("java.vm.name") + " by " + System.getProperty("java.vendor")); Hyperlink link = new Hyperlink("GitHub"); link.setOnAction(event -> editor.getHostServices().showDocument("https://github.com/acmi/xdat_editor")); Label license = new Label(interfaceResources.getString("help.open_source_licenses")); Hyperlink licenseApache = new Hyperlink("Apache Commons IO, Apache Commons CSV,\nApache Commons Lang, Groovy"); licenseApache.setOnAction(event -> editor.getHostServices().showDocument("http://www.apache.org/licenses/LICENSE-2.0.txt")); Hyperlink licenseControlsFX = new Hyperlink("ControlsFX"); licenseControlsFX.setOnAction(event -> editor.getHostServices().showDocument("https://bitbucket.org/controlsfx/controlsfx/raw/15b3171c215f00de751a37d14f6b678d6896f8a2/license.txt")); VBox content = new VBox(name, version, jre, jvm, link, license, licenseApache, licenseControlsFX); VBox.setMargin(jre, new Insets(10, 0, 0, 0)); VBox.setMargin(link, new Insets(10, 0, 0, 0)); VBox.setMargin(license, new Insets(15, 0, 0, 0)); DialogPane pane = new DialogPane(); pane.setContent(content); pane.getButtonTypes().addAll(ButtonType.OK); dialog.setDialogPane(pane); dialog.showAndWait(); } private static class ListHolder { IOEntity entity; List<IOEntity> list; String name; Class<? extends IOEntity> type; ListHolder(IOEntity entity, List<IOEntity> list, String name, Class<? extends IOEntity> type) { this.entity = entity; this.list = list; this.name = name; this.type = type; } @Override public String toString() { return name; } } private static class ClassHolder { Class<? extends IOEntity> clazz; private ClassHolder(Class<? extends IOEntity> clazz) { this.clazz = clazz; } @Override public String toString() { return clazz.getSimpleName(); } } }