Java tutorial
/* * 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.*; 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.layout.AnchorPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; 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.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.*; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; 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 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 new Environment(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 = new Environment(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.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 -> { log.log(Level.WARNING, String.format("%s: XDAT class load error", name), e); Platform.runLater(() -> { version.getToggles().remove(menuItem); versionMenu.getItems().remove(menuItem); Dialogs.show(Alert.AlertType.ERROR, name + ": XDAT class load error", null, e.getClass().getSimpleName() + ": " + e.getMessage()); }); }); } }); 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.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 { List<IOEntity> list = (List<IOEntity>) listField.get(entity); if (!listField.isAnnotationPresent(Type.class)) { log.log(Level.WARNING, String.format("XDAT.%s: @Type not defined", listField.getName())); Dialogs.show(Alert.AlertType.ERROR, "ReflectiveOperationException", null, String.format("XDAT.%s: @Type not defined", listField.getName())); } 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) { log.log(Level.WARNING, String.format("%s.%s is not accessible", listField.getDeclaringClass().getSimpleName(), listField.getName()), e); Dialogs.show(Alert.AlertType.ERROR, "ReflectiveOperationException", null, listField.getDeclaringClass().getSimpleName() + "." + listField.getName() + " is not accessible"); } } 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(); int index = parent.list.indexOf(value); 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) { log.log(Level.WARNING, String.format("Couldn't instantiate %s", toCreate.clazz.getName()), e); Dialogs.show(Alert.AlertType.ERROR, "ReflectiveOperationException", null, "Couldn't instantiate " + toCreate.clazz); } }); }); 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) { log.log(Level.WARNING, String.format("%s.%s is not accessible", o.getClass(), field.getName()), e); Dialogs.show(Alert.AlertType.ERROR, "ReflectiveOperationException", null, String.format("%s.%s is not accessible", o.getClass(), field.getName())); } obj.ifPresent(val -> { if (List.class.isAssignableFrom(field.getType())) { if (!field.isAnnotationPresent(Type.class)) { log.log(Level.WARNING, String.format("%s.%s: @Type not defined", o.getClass().getName(), field.getName())); Dialogs.show(Alert.AlertType.ERROR, "ReflectiveOperationException", null, String.format("%s.%s: @Type not defined", o.getClass().getName(), field.getName())); } else { 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, newValue) -> editor.getHistory() .valueChanged(treeItemToScriptString(newSelection), property.getName(), newValue); 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) { try { List<String> names = Arrays.stream(objClass.getDeclaredFields()) .map(field -> field.getName().replace("Prop", "")).collect(Collectors.toList()); BeanInfo beanInfo = Introspector.getBeanInfo(objClass, objClass.getSuperclass()); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); Arrays.sort(propertyDescriptors, (pd1, pd2) -> Integer.compare(names.indexOf(pd1.getName()), names.indexOf(pd2.getName()))); for (PropertyDescriptor descriptor : propertyDescriptors) { if ("metaClass".equals(descriptor.getName())) continue; if (Collection.class.isAssignableFrom(descriptor.getPropertyType())) continue; AnnotatedElement getter = descriptor.getReadMethod(); if (getter.isAnnotationPresent(Deprecated.class) || getter.isAnnotationPresent(Hide.class)) continue; String description = ""; if (getter.isAnnotationPresent(Description.class)) description = getter.getAnnotation(Description.class).value(); Class<? extends PropertyEditor<?>> propertyEditorClass = null; if (descriptor.getPropertyType() == Boolean.class || descriptor.getPropertyType() == Boolean.TYPE) { propertyEditorClass = BooleanPropertyEditor.class; } else if (getter.isAnnotationPresent(Tex.class)) { propertyEditorClass = TexturePropertyEditor.class; } else if (getter.isAnnotationPresent(Sysstr.class)) { propertyEditorClass = SysstringPropertyEditor.class; } BeanProperty property = new BeanProperty(descriptor, objClass.getSimpleName(), description, propertyEditorClass); list.add(property); } } catch (IntrospectionException e) { e.printStackTrace(); } 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('[').append(holder.list.indexOf(list.get(++i).getValue())).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 { 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 (Exception e) { log.log(Level.WARNING, String.format("Read error before offset 0x%x", cis.getCount()), e); throw e; } return null; }, e -> Platform.runLater(() -> Dialogs.show(Alert.AlertType.ERROR, "Read error", null, "Try to choose another version"))); } catch (ReflectiveOperationException e) { log.log(Level.WARNING, "XDAT class should have empty public constructor", e); Dialogs.show(Alert.AlertType.ERROR, "ReflectiveOperationException", null, "XDAT class should have empty public constructor"); } } @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 -> { log.log(Level.WARNING, "Write error", e); Platform.runLater( () -> Dialogs.show(Alert.AlertType.ERROR, e.getClass().getSimpleName(), null, e.getMessage())); }); } @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() { Dialogs.show(Alert.AlertType.INFORMATION, "About", null, new Label("XDAT Editor\n" + "Version: " + editor.getApplicationVersion())); } 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(); } } }