/*
* Nocturne
* Copyright (c) 2015-2016, Lapis <https://github.com/LapisBlue>
*
* The MIT License
*
* 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 blue.lapis.nocturne.gui;
import static com.google.common.base.Preconditions.checkArgument;
import blue.lapis.nocturne.Main;
import blue.lapis.nocturne.gui.io.jar.JarDialogHelper;
import blue.lapis.nocturne.gui.io.mappings.MappingsOpenDialogHelper;
import blue.lapis.nocturne.gui.io.mappings.MappingsSaveDialogHelper;
import blue.lapis.nocturne.gui.scene.control.CodeTab;
import blue.lapis.nocturne.gui.scene.control.IdentifiableTreeItem;
import blue.lapis.nocturne.gui.scene.text.SelectableMember;
import blue.lapis.nocturne.jar.model.JarClassEntry;
import blue.lapis.nocturne.jar.model.hierarchy.Hierarchy;
import blue.lapis.nocturne.jar.model.hierarchy.HierarchyElement;
import blue.lapis.nocturne.jar.model.hierarchy.HierarchyNode;
import blue.lapis.nocturne.util.Constants;
import blue.lapis.nocturne.util.helper.PropertiesHelper;
import blue.lapis.nocturne.util.helper.SceneHelper;
import javafx.event.ActionEvent;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.TabPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
/**
* The main JavaFX controller.
*/
public class MainController implements Initializable {
public static MainController INSTANCE;
private static final Alert RESTART_ALERT = new Alert(Alert.AlertType.WARNING);
public MenuItem openJarButton;
public MenuItem closeJarButton;
public MenuItem loadMappingsButton;
public MenuItem mergeMappingsButton;
public MenuItem saveMappingsButton;
public MenuItem saveMappingsAsButton;
public MenuItem closeButton;
public MenuItem resetMappingsButton;
public ToggleGroup languageGroup;
public MenuItem aboutButton;
public TabPane tabs;
public TreeView<String> obfTree;
public TreeView<String> deobfTree;
public MainController() {
INSTANCE = this;
}
@Override
public void initialize(URL location, ResourceBundle resources) {
closeJarButton.setDisable(Main.getLoadedJar() == null);
loadMappingsButton.setDisable(Main.getLoadedJar() == null);
mergeMappingsButton.setDisable(Main.getLoadedJar() == null);
saveMappingsButton.setDisable(Main.getLoadedJar() == null);
saveMappingsAsButton.setDisable(Main.getLoadedJar() == null);
resetMappingsButton.setDisable(Main.getLoadedJar() == null);
final String langRadioPrefix = "langRadio-";
for (Toggle toggle : languageGroup.getToggles()) {
if (((RadioMenuItem) toggle).getId().equals(langRadioPrefix + Main.getCurrentLocale())) {
toggle.setSelected(true);
break;
}
}
setAccelerators();
this.initTreeViews();
RESTART_ALERT.setTitle(Main.getResourceBundle().getString("dialog.restart.title"));
RESTART_ALERT.setHeaderText(null);
RESTART_ALERT.setContentText(Main.getResourceBundle().getString("dialog.restart.content"));
}
private void initTreeViews() {
BiConsumer<InputEvent, TreeView<String>> clickHandler = (event, treeView) -> {
if ((event instanceof MouseEvent && ((MouseEvent) event).getClickCount() == 2)
|| (event instanceof KeyEvent && ((KeyEvent) event).getCode() == KeyCode.ENTER)) {
TreeItem<String> selected = treeView.getSelectionModel().getSelectedItem();
if (selected == null) {
return;
}
if (selected.getChildren().isEmpty()) {
String className = ((IdentifiableTreeItem) selected).getId().substring(1);
if (Main.getLoadedJar() != null) {
openTab(className, selected.getValue());
}
} else {
if (event instanceof MouseEvent == selected.isExpanded()) {
selected.setExpanded(true);
while (selected.getChildren().size() == 1) {
selected = selected.getChildren().get(0);
selected.setExpanded(true);
}
} else {
selected.setExpanded(false);
}
}
}
};
obfTree.setOnMouseClicked(event -> clickHandler.accept(event, obfTree));
deobfTree.setOnMouseClicked(event -> clickHandler.accept(event, deobfTree));
obfTree.setOnKeyReleased(event -> clickHandler.accept(event, obfTree));
deobfTree.setOnKeyReleased(event -> clickHandler.accept(event, deobfTree));
}
private void setAccelerators() {
openJarButton.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN));
loadMappingsButton.setAccelerator(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN));
mergeMappingsButton.setAccelerator(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN,
KeyCombination.ALT_DOWN));
saveMappingsButton.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN));
saveMappingsAsButton.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN,
KeyCombination.ALT_DOWN));
aboutButton.setAccelerator(new KeyCodeCombination(KeyCode.F1));
}
public void openJar(ActionEvent actionEvent) throws IOException {
if (Main.getLoadedJar() != null && !deinitializeCurrentJar()) {
return;
}
JarDialogHelper.openJar(this);
updateClassViews();
}
public void closeJar(ActionEvent actionEvent) throws IOException {
if (!deinitializeCurrentJar()) {
return;
}
closeJarButton.setDisable(true);
loadMappingsButton.setDisable(true);
mergeMappingsButton.setDisable(true);
saveMappingsButton.setDisable(true);
saveMappingsAsButton.setDisable(true);
resetMappingsButton.setDisable(true);
Main.getMappingContext().clear();
Main.getMappingContext().setDirty(false);
updateClassViews();
}
public void loadMappings(ActionEvent actionEvent) throws IOException {
try {
if (MappingsSaveDialogHelper.doDirtyConfirmation()) {
return;
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
MappingsOpenDialogHelper.openMappings(false);
updateClassViews();
}
public void mergeMappings(ActionEvent actionEvent) throws IOException {
MappingsOpenDialogHelper.openMappings(true);
updateClassViews();
}
public void resetMappings(ActionEvent actionEvent) {
try {
if (MappingsSaveDialogHelper.doDirtyConfirmation()) {
return;
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
Main.getMappingContext().getMappings().values().forEach(cm -> {
Main.getLoadedJar().getCurrentNames().put(cm.getObfuscatedName(), cm.getObfuscatedName());
JarClassEntry jce = Main.getLoadedJar().getClass(cm.getObfuscatedName()).orElse(null);
if (jce == null) {
return;
}
cm.getInnerClassMappings().values()
.forEach(im -> jce.getCurrentInnerClassNames().put(im.getObfuscatedName(), im.getObfuscatedName()));
cm.getFieldMappings().values()
.forEach(fm -> jce.getCurrentFields().put(fm.getSignature(), fm.getSignature()));
cm.getMethodMappings().values()
.forEach(mm -> jce.getCurrentMethods().put(mm.getSignature(), mm.getSignature()));
});
Main.getMappingContext().clear();
Main.getLoadedJar().getClasses().forEach(jce -> jce.setDeobfuscated(false));
CodeTab.CODE_TABS.values().forEach(CodeTab::resetClassName);
SelectableMember.MEMBERS.values()
.forEach(list -> list.forEach(member -> {
member.setAndProcessText(member.getName());
member.setDeobfuscated(false);
}));
updateClassViews();
}
public void saveMappings(ActionEvent actionEvent) throws IOException {
MappingsSaveDialogHelper.saveMappings();
}
public void saveMappingsAs(ActionEvent actionEvent) throws IOException {
MappingsSaveDialogHelper.saveMappingsAs();
}
public void onClose(ActionEvent actionEvent) {
try {
if (MappingsSaveDialogHelper.doDirtyConfirmation()) {
return;
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
System.exit(0);
}
public void showAbout(ActionEvent actionEvent) throws IOException {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(Main.getResourceBundle().getString("about.title"));
alert.setHeaderText("Nocturne v" + Constants.VERSION);
alert.getDialogPane().getStyleClass().add("about");
SceneHelper.addStdStylesheet(alert.getDialogPane());
FXMLLoader loader = new FXMLLoader(ClassLoader.getSystemResource("fxml/about.fxml"));
loader.setResources(Main.getResourceBundle());
Node content = loader.load();
alert.getDialogPane().setContent(content);
alert.showAndWait();
}
public void onLanguageSelect(ActionEvent actionEvent) throws IOException {
RadioMenuItem radioItem = (RadioMenuItem) actionEvent.getSource();
final String langPrefix = "langRadio-";
String langId = radioItem.getId().substring(langPrefix.length());
if (!langId.equals(Main.getCurrentLocale())) {
Main.getPropertiesHelper().setProperty(PropertiesHelper.Key.LOCALE, langId);
RESTART_ALERT.showAndWait();
}
}
public void updateObfuscatedClassListView() {
if (Main.getLoadedJar() != null) {
TreeItem<String> root = generateTreeItem(Main.getLoadedJar().getObfuscatedHierarchy(),
getExpandedIds((IdentifiableTreeItem) obfTree.getRoot()));
root.setExpanded(true);
obfTree.setRoot(root);
} else {
obfTree.setRoot(null);
}
}
public void updateDeobfuscatedClassListView() {
if (Main.getLoadedJar() != null) {
TreeItem<String> root = generateTreeItem(Main.getLoadedJar().getDeobfuscatedHierarchy(),
getExpandedIds((IdentifiableTreeItem) deobfTree.getRoot()));
root.setExpanded(true);
deobfTree.setRoot(root);
} else {
deobfTree.setRoot(null);
}
}
public TreeItem<String> generateTreeItem(HierarchyElement element, Set<String> expanded) {
IdentifiableTreeItem treeItem;
if (element instanceof HierarchyNode) {
HierarchyNode node = (HierarchyNode) element;
treeItem = new IdentifiableTreeItem((node.isTerminal() ? "C" : "P") + node.getId(), node.getDisplayName());
} else {
treeItem = new IdentifiableTreeItem("//root", "(root)");
}
if (expanded.contains(treeItem.getId())) {
treeItem.setExpanded(true);
}
if (element instanceof Hierarchy
|| (element instanceof HierarchyNode && !((HierarchyNode) element).isTerminal())) {
treeItem.getChildren().addAll(element.getChildren().stream()
.map(e -> this.generateTreeItem(e, expanded)).collect(Collectors.toList()));
}
treeItem.getChildren().setAll(treeItem.getChildren().sorted((t1, t2) -> {
boolean c1 = t1.getChildren().size() > 0;
boolean c2 = t2.getChildren().size() > 0;
if (c1 == c2) { // both either terminal or non-terminal
return t1.getValue().compareTo(t2.getValue());
} else if (c1) { // first is non-terminal, second is terminal
return -1;
} else { // first is terminal, second is non-terminal
return 1;
}
}));
return treeItem;
}
public void updateClassViews() {
updateObfuscatedClassListView();
updateDeobfuscatedClassListView();
}
private boolean deinitializeCurrentJar() throws IOException {
if (MappingsSaveDialogHelper.doDirtyConfirmation()) {
return false;
}
Main.getMappingContext().clear();
closeAllTabs();
Main.setLoadedJar(null);
return true;
}
/**
* Closes all currently opened tabs.
*/
private void closeAllTabs() {
tabs.getTabs().forEach(tab -> tab.getOnClosed().handle(null));
tabs.getTabs().clear();
CodeTab.CODE_TABS.clear();
}
public void openTab(String className, String displayName) {
if (CodeTab.CODE_TABS.containsKey(className)) {
tabs.getSelectionModel().select(CodeTab.CODE_TABS.get(className));
} else {
CodeTab tab = new CodeTab(tabs, className, displayName);
Optional<JarClassEntry> clazz = Main.getLoadedJar().getClass(className);
checkArgument(clazz.isPresent(), "Cannot find class entry for " + className);
tab.setCode(clazz.get().decompile());
}
}
public static boolean isInitialized() {
return INSTANCE != null;
}
private static Map<String, IdentifiableTreeItem> flatten(IdentifiableTreeItem tree) {
Map<String, IdentifiableTreeItem> map = new HashMap<>();
map.put(tree.getId(), tree);
if (tree.getChildren().isEmpty()) {
return map;
}
for (TreeItem<String> child : tree.getChildren()) {
map.putAll(flatten((IdentifiableTreeItem) child));
}
return map;
}
private static Set<String> getExpandedIds(IdentifiableTreeItem tree) {
if (tree == null) {
return Collections.emptySet();
}
return flatten(tree).entrySet().stream().filter(e -> e.getValue().isExpanded()).map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
}