diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/PrintStatsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/PrintStatsCommand.java index 47d8067c0..2ed1b3078 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/PrintStatsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/PrintStatsCommand.java @@ -3,6 +3,7 @@ import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.EnigmaPlugin; import org.quiltmc.enigma.api.EnigmaProfile; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.ProjectStatsResult; import org.quiltmc.enigma.api.stats.StatType; import org.quiltmc.enigma.api.stats.StatsGenerator; @@ -47,7 +48,7 @@ public static void run(Path inJar, Path mappings, @Nullable Path profilePath, @N public static void run(Path inJar, Path mappings, Enigma enigma) throws Exception { StatsGenerator generator = new StatsGenerator(openProject(inJar, mappings, enigma)); - ProjectStatsResult result = generator.generate(new ConsoleProgressListener(), Set.of(StatType.values()), false); + ProjectStatsResult result = generator.generate(new ConsoleProgressListener(), new GenerationParameters(Set.of(StatType.values()))); Logger.info(String.format("Overall mapped: %.2f%% (%s / %s)", result.getPercentage(), result.getMapped(), result.getMappable())); Logger.info(String.format("Classes: %.2f%% (%s / %s)", result.getPercentage(StatType.CLASSES), result.getMapped(StatType.CLASSES), result.getMappable(StatType.CLASSES))); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java index 9a86ebdc6..fd0da33fb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java @@ -26,7 +26,7 @@ import org.quiltmc.enigma.gui.docker.StructureDocker; import org.quiltmc.enigma.gui.element.EditorTabbedPane; import org.quiltmc.enigma.gui.element.MainWindow; -import org.quiltmc.enigma.gui.element.MenuBar; +import org.quiltmc.enigma.gui.element.menu_bar.MenuBar; import org.quiltmc.enigma.gui.panel.EditorPanel; import org.quiltmc.enigma.gui.panel.IdentifierPanel; import org.quiltmc.enigma.gui.renderer.MessageListCellRenderer; @@ -282,7 +282,7 @@ public boolean isTestEnvironment() { public void addCrash(Throwable t) { this.crashHistory.add(t); - this.menuBar.prepareCrashHistoryMenu(); + this.menuBar.getFileMenu().prepareCrashHistoryMenu(); } public DockerManager getDockerManager() { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java index cb1154f8d..76728985e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java @@ -14,6 +14,7 @@ import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.analysis.tree.FieldReferenceTreeNode; import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.gui.dialog.CrashDialog; import org.quiltmc.enigma.gui.docker.ClassesDocker; @@ -166,7 +167,7 @@ public CompletableFuture openMappings(ReadWriteService readWriteService, P this.gui.setMappingsFile(path); Config.insertRecentProject(this.project.getJarPath().toString(), path.toString()); - this.gui.getMenuBar().reloadOpenRecentMenu(this.gui); + this.gui.getMenuBar().getFileMenu().reloadOpenRecentMenu(); return ProgressDialog.runOffThread(this.gui, progress -> { try { @@ -179,18 +180,7 @@ public CompletableFuture openMappings(ReadWriteService readWriteService, P this.refreshClasses(); this.chp.invalidateJavadoc(); this.statsGenerator = new StatsGenerator(this.project); - new Thread(() -> { - ProgressListener progressListener = ProgressListener.createEmpty(); - this.gui.getMainWindow().getStatusBar().syncWith(progressListener); - this.statsGenerator.generate(progressListener, EditableType.toStatTypes(this.gui.getEditableTypes()), false); - - // ensure all class tree dockers show the update to the stats icons - for (Docker docker : this.gui.getDockerManager().getActiveDockers().values()) { - if (docker instanceof ClassesDocker) { - docker.repaint(); - } - } - }).start(); + new Thread(this::regenerateAndUpdateStatIcons).start(); } catch (MappingParseException e) { JOptionPane.showMessageDialog(this.gui.getFrame(), e.getMessage()); } catch (Exception e) { @@ -208,6 +198,26 @@ public void openMappings(EntryTree mappings) { this.chp.invalidateJavadoc(); } + public void regenerateAndUpdateStatIcons() { + if (Config.main().features.enableClassTreeStatIcons.value()) { + ProgressListener progressListener = ProgressListener.createEmpty(); + this.gui.getMainWindow().getStatusBar().syncWith(progressListener); + + var includedTypes = EditableType.toStatTypes(this.gui.getEditableTypes()); + includedTypes.removeIf(type -> !Config.stats().includedStatTypes.value().contains(type)); + + GenerationParameters parameters = new GenerationParameters(includedTypes, Config.stats().shouldIncludeSyntheticParameters.value(), Config.stats().shouldCountFallbackNames.value()); + this.statsGenerator.generate(progressListener, parameters); + } + + // ensure all class tree dockers show the update to the stats icons + for (Docker docker : this.gui.getDockerManager().getActiveDockers().values()) { + if (docker instanceof ClassesDocker) { + docker.repaint(); + } + } + } + public CompletableFuture saveMappings(Path path) { return this.saveMappings(path, this.readWriteService); } @@ -594,7 +604,7 @@ private void applyChange0(ValidationContext vc, EntryChange change, boolean u public void openStatsTree(Set includedTypes) { ProgressDialog.runOffThread(this.gui, progress -> { - StatsResult overall = this.getStatsGenerator().getResult(EditableType.toStatTypes(this.gui.getEditableTypes()), false).getOverall(); + StatsResult overall = this.getStatsGenerator().getResult(new GenerationParameters(EditableType.toStatTypes(this.gui.getEditableTypes()))).getOverall(); StatsTree tree = overall.buildTree(Config.main().stats.lastTopLevelPackage.value(), includedTypes); String treeJson = GSON.toJson(tree.root); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java index afa4fa203..e00f7ba28 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java @@ -109,6 +109,10 @@ public static Config main() { return MAIN; } + public static StatsSection stats() { + return main().stats; + } + public static DockerConfig dockers() { return DOCKER; } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java index 11027a047..244075e47 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java @@ -4,10 +4,17 @@ import org.quiltmc.config.api.annotations.SerializedNameConvention; import org.quiltmc.config.api.metadata.NamingSchemes; import org.quiltmc.config.api.values.TrackedValue; +import org.quiltmc.config.api.values.ValueList; +import org.quiltmc.enigma.api.stats.StatType; + +import java.util.EnumSet; @SerializedNameConvention(NamingSchemes.SNAKE_CASE) public class StatsSection extends ReflectiveConfig.Section { public final TrackedValue lastSelectedDir = this.value(""); public final TrackedValue lastTopLevelPackage = this.value(""); + + public final TrackedValue> includedStatTypes = this.list(StatType.CLASSES, EnumSet.allOf(StatType.class).toArray(StatType[]::new)); public final TrackedValue shouldIncludeSyntheticParameters = this.value(false); + public final TrackedValue shouldCountFallbackNames = this.value(false); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/StatsDialog.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/StatsDialog.java index d54f68952..4e80ea2a1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/StatsDialog.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/StatsDialog.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.dialog; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.StatsGenerator; import org.quiltmc.enigma.gui.EditableType; import org.quiltmc.enigma.gui.Gui; @@ -38,7 +39,7 @@ public static void show(Gui gui) { listener.sync(generator.getOverallProgress()); } - ProjectStatsResult result = gui.getController().getStatsGenerator().getResult(EditableType.toStatTypes(gui.getEditableTypes()), false); + ProjectStatsResult result = gui.getController().getStatsGenerator().getResult(new GenerationParameters(EditableType.toStatTypes(gui.getEditableTypes()))); SwingUtilities.invokeLater(() -> show(gui, result, "")); }); } else { @@ -90,6 +91,11 @@ public static void show(Gui gui, ProjectStatsResult result, String packageName) syntheticParametersOption.setSelected(Config.main().stats.shouldIncludeSyntheticParameters.value()); contentPane.add(syntheticParametersOption, cb1.pos(0, result.getOverall().getTypes().size() + 4).build()); + // show synthetic members option + JCheckBox countFallbackOption = new JCheckBox(I18n.translate("menu.file.stats.count_fallback")); + countFallbackOption.setSelected(Config.main().stats.shouldCountFallbackNames.value()); + contentPane.add(countFallbackOption, cb1.pos(0, result.getOverall().getTypes().size() + 3).build()); + // show filter button JButton filterButton = new JButton(I18n.translate("menu.file.stats.filter")); filterButton.addActionListener(action -> { @@ -98,11 +104,14 @@ public static void show(Gui gui, ProjectStatsResult result, String packageName) String topLevelPackageSlashes = topLevelPackage.getText().replace('.', '/'); Config.main().stats.lastTopLevelPackage.setValue(topLevelPackage.getText(), true); - ProjectStatsResult projectResult = gui.getController().getStatsGenerator().getResult(EditableType.toStatTypes(gui.getEditableTypes()), syntheticParametersOption.isSelected()).filter(topLevelPackageSlashes); + GenerationParameters parameters = new GenerationParameters(EditableType.toStatTypes(gui.getEditableTypes()), syntheticParametersOption.isSelected(), countFallbackOption.isSelected()); + StatsGenerator generator = gui.getController().getStatsGenerator(); + ProjectStatsResult projectResult = generator.getResult(parameters).filter(topLevelPackageSlashes); + SwingUtilities.invokeLater(() -> show(gui, projectResult, topLevelPackageSlashes)); }); }); - contentPane.add(filterButton, cb1.pos(0, result.getOverall().getTypes().size() + 3).anchor(GridBagConstraints.EAST).build()); + contentPane.add(filterButton, cb1.pos(0, result.getOverall().getTypes().size() + 5).anchor(GridBagConstraints.EAST).build()); // show generate button JButton button = new JButton(I18n.translate("menu.file.stats.generate")); @@ -110,8 +119,7 @@ public static void show(Gui gui, ProjectStatsResult result, String packageName) button.addActionListener(action -> { dialog.dispose(); - Config.main().stats.lastTopLevelPackage.setValue(topLevelPackage.getText(), true); - Config.main().stats.shouldIncludeSyntheticParameters.setValue(syntheticParametersOption.isSelected(), true); + Config.main().stats.lastTopLevelPackage.setValue(topLevelPackage.getText()); generateStats(gui, checkboxes); }); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java index abdca1450..eda5a880d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/CollabDocker.java @@ -54,8 +54,8 @@ public CollabDocker(Gui gui) { connectionButtonPanel.add(this.startServerButton, BorderLayout.NORTH); connectionButtonPanel.add(this.connectToServerButton, BorderLayout.SOUTH); - this.startServerButton.addActionListener(e -> this.gui.getMenuBar().onStartServerClicked()); - this.connectToServerButton.addActionListener(e -> this.gui.getMenuBar().onConnectClicked()); + this.startServerButton.addActionListener(e -> this.gui.getMenuBar().getCollabMenu().onStartServerClicked()); + this.connectToServerButton.addActionListener(e -> this.gui.getMenuBar().getCollabMenu().onConnectClicked()); // we make a copy of the title bar to avoid having to shuffle it around both panels this.titleCopy = new DockerTitleBar(gui, this, this.titleSupplier); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java index 3e2148fff..817d49bf6 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java @@ -34,14 +34,14 @@ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean if ((this.controller.getProject() != null && leaf && value instanceof ClassSelectorClassNode) || (this.controller.getProject() != null && value instanceof ClassSelectorPackageNode)) { - TooltipPanel panel; + StatsTooltipPanel panel; Icon icon; Function deobfuscationIconGetter; Runnable reloader; if (value instanceof ClassSelectorPackageNode node) { - class PackageTooltipPanel extends TooltipPanel { - PackageTooltipPanel(GuiController controller) { + class PackageStatsTooltipPanel extends StatsTooltipPanel { + PackageStatsTooltipPanel(GuiController controller) { super(controller); } @@ -56,15 +56,15 @@ String getDisplayName() { } } - panel = new PackageTooltipPanel(this.controller); + panel = new PackageStatsTooltipPanel(this.controller); icon = GuiUtil.getFolderIcon(this, tree, node); deobfuscationIconGetter = projectStatsResult -> GuiUtil.getDeobfuscationIcon(projectStatsResult, node.getPackageName()); reloader = () -> {}; } else { ClassSelectorClassNode node = (ClassSelectorClassNode) value; - class ClassTooltipPanel extends TooltipPanel { - ClassTooltipPanel(GuiController controller) { + class ClassStatsTooltipPanel extends StatsTooltipPanel { + ClassStatsTooltipPanel(GuiController controller) { super(controller); } @@ -79,7 +79,7 @@ String getDisplayName() { } } - panel = new ClassTooltipPanel(this.controller); + panel = new ClassStatsTooltipPanel(this.controller); icon = GuiUtil.getClassIcon(this.controller.getGui(), node.getObfEntry()); deobfuscationIconGetter = projectStatsResult -> GuiUtil.getDeobfuscationIcon(projectStatsResult, node.getObfEntry()); reloader = () -> node.reloadStats(this.controller.getGui(), this.selector, false); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java deleted file mode 100644 index 5535548ce..000000000 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java +++ /dev/null @@ -1,700 +0,0 @@ -package org.quiltmc.enigma.gui.element; - -import org.quiltmc.enigma.api.service.ReadWriteService; -import org.quiltmc.enigma.gui.ConnectionState; -import org.quiltmc.enigma.gui.Gui; -import org.quiltmc.enigma.gui.NotificationManager; -import org.quiltmc.enigma.gui.config.Decompiler; -import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.config.keybind.KeyBinds; -import org.quiltmc.enigma.gui.dialog.AboutDialog; -import org.quiltmc.enigma.gui.dialog.ChangeDialog; -import org.quiltmc.enigma.gui.dialog.ConnectToServerDialog; -import org.quiltmc.enigma.gui.dialog.CrashDialog; -import org.quiltmc.enigma.gui.dialog.CreateServerDialog; -import org.quiltmc.enigma.gui.dialog.FontDialog; -import org.quiltmc.enigma.gui.dialog.SearchDialog; -import org.quiltmc.enigma.gui.dialog.StatsDialog; -import org.quiltmc.enigma.gui.dialog.decompiler.DecompilerSettingsDialog; -import org.quiltmc.enigma.gui.dialog.keybind.ConfigureKeyBindsDialog; -import org.quiltmc.enigma.gui.util.ExtensionFileFilter; -import org.quiltmc.enigma.gui.util.GuiUtil; -import org.quiltmc.enigma.gui.util.LanguageUtil; -import org.quiltmc.enigma.gui.util.ScaleUtil; -import org.quiltmc.enigma.util.I18n; -import org.quiltmc.enigma.util.Pair; -import org.quiltmc.enigma.util.validation.Message; -import org.quiltmc.enigma.util.validation.ParameterizedMessage; - -import javax.annotation.Nullable; -import javax.swing.ButtonGroup; -import javax.swing.JFileChooser; -import javax.swing.JMenu; -import javax.swing.JMenuBar; -import javax.swing.JMenuItem; -import javax.swing.JOptionPane; -import javax.swing.JRadioButtonMenuItem; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -public class MenuBar { - private final JMenu fileMenu = new JMenu(); - private final JMenuItem jarOpenItem = new JMenuItem(); - private final JMenuItem jarCloseItem = new JMenuItem(); - private final JMenuItem openMappingsItem = new JMenuItem(); - private final JMenu openRecentMenu = new JMenu(); - private final JMenuItem maxRecentFilesItem = new JMenuItem(); - private final JMenuItem saveMappingsItem = new JMenuItem(); - private final JMenu saveMappingsAsMenu = new JMenu(); - private final JMenuItem closeMappingsItem = new JMenuItem(); - private final JMenuItem dropMappingsItem = new JMenuItem(); - private final JMenuItem reloadMappingsItem = new JMenuItem(); - private final JMenuItem reloadAllItem = new JMenuItem(); - private final JMenuItem exportSourceItem = new JMenuItem(); - private final JMenuItem exportJarItem = new JMenuItem(); - private final JMenuItem statsItem = new JMenuItem(); - private final JMenuItem configureKeyBindsItem = new JMenuItem(); - private final JMenuItem exitItem = new JMenuItem(); - private final JMenu crashHistoryMenu = new JMenu(); - - private final JMenu decompilerMenu = new JMenu(); - private final JMenuItem decompilerSettingsItem = new JMenuItem(); - - private final JMenu viewMenu = new JMenu(); - private final JMenu themesMenu = new JMenu(); - private final JMenu languagesMenu = new JMenu(); - private final JMenu scaleMenu = new JMenu(); - private final JMenu notificationsMenu = new JMenu(); - private final JMenuItem fontItem = new JMenuItem(); - private final JMenuItem customScaleItem = new JMenuItem(); - - private final JMenu searchMenu = new JMenu(); - private final JMenuItem searchItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); - private final JMenuItem searchAllItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); - private final JMenuItem searchClassItem = new JMenuItem(GuiUtil.CLASS_ICON); - private final JMenuItem searchMethodItem = new JMenuItem(GuiUtil.METHOD_ICON); - private final JMenuItem searchFieldItem = new JMenuItem(GuiUtil.FIELD_ICON); - - private final JMenu collabMenu = new JMenu(); - private final JMenuItem connectItem = new JMenuItem(); - private final JMenuItem startServerItem = new JMenuItem(); - - private final JMenu helpMenu = new JMenu(); - private final JMenuItem aboutItem = new JMenuItem(); - private final JMenuItem githubItem = new JMenuItem(); - - // Enabled with system property "enigma.development" or "--development" flag - private final DevMenu devMenu; - - private final Gui gui; - - public MenuBar(Gui gui) { - this.gui = gui; - this.devMenu = new DevMenu(gui); - - JMenuBar ui = gui.getMainWindow().getMenuBar(); - - this.retranslateUi(); - - this.reloadOpenRecentMenu(gui); - prepareSaveMappingsAsMenu(this.saveMappingsAsMenu, this.saveMappingsItem, gui); - prepareDecompilerMenu(this.decompilerMenu, this.decompilerSettingsItem, gui); - prepareThemesMenu(this.themesMenu, gui); - prepareLanguagesMenu(this.languagesMenu); - prepareScaleMenu(this.scaleMenu, gui); - prepareNotificationsMenu(this.notificationsMenu); - this.prepareCrashHistoryMenu(); - - this.fileMenu.add(this.jarOpenItem); - this.fileMenu.add(this.jarCloseItem); - this.fileMenu.addSeparator(); - this.fileMenu.add(this.openRecentMenu); - this.fileMenu.add(this.maxRecentFilesItem); - this.fileMenu.addSeparator(); - this.fileMenu.add(this.openMappingsItem); - this.fileMenu.add(this.saveMappingsItem); - this.fileMenu.add(this.saveMappingsAsMenu); - this.fileMenu.add(this.closeMappingsItem); - this.fileMenu.add(this.dropMappingsItem); - this.fileMenu.addSeparator(); - this.fileMenu.add(this.reloadMappingsItem); - this.fileMenu.add(this.reloadAllItem); - this.fileMenu.addSeparator(); - this.fileMenu.add(this.exportSourceItem); - this.fileMenu.add(this.exportJarItem); - this.fileMenu.addSeparator(); - this.fileMenu.add(this.statsItem); - this.fileMenu.addSeparator(); - this.fileMenu.add(this.configureKeyBindsItem); - this.fileMenu.addSeparator(); - this.fileMenu.add(this.crashHistoryMenu); - this.fileMenu.add(this.exitItem); - ui.add(this.fileMenu); - - ui.add(this.decompilerMenu); - - this.viewMenu.add(this.themesMenu); - this.viewMenu.add(this.languagesMenu); - this.viewMenu.add(this.notificationsMenu); - this.scaleMenu.add(this.customScaleItem); - this.viewMenu.add(this.scaleMenu); - this.viewMenu.add(this.fontItem); - ui.add(this.viewMenu); - - this.searchMenu.add(this.searchItem); - this.searchMenu.add(this.searchAllItem); - this.searchMenu.add(this.searchClassItem); - this.searchMenu.add(this.searchMethodItem); - this.searchMenu.add(this.searchFieldItem); - ui.add(this.searchMenu); - - this.collabMenu.add(this.connectItem); - this.collabMenu.add(this.startServerItem); - ui.add(this.collabMenu); - - this.helpMenu.add(this.aboutItem); - this.helpMenu.add(this.githubItem); - ui.add(this.helpMenu); - - if (System.getProperty("enigma.development", "false").equalsIgnoreCase("true") || Config.main().development.anyEnabled) { - ui.add(this.devMenu); - } - - this.setKeyBinds(); - - this.jarOpenItem.addActionListener(e -> this.onOpenJarClicked()); - this.openMappingsItem.addActionListener(e -> this.onOpenMappingsClicked()); - this.jarCloseItem.addActionListener(e -> this.gui.getController().closeJar()); - this.maxRecentFilesItem.addActionListener(e -> this.onMaxRecentFilesClicked()); - this.saveMappingsItem.addActionListener(e -> this.onSaveMappingsClicked()); - this.closeMappingsItem.addActionListener(e -> this.onCloseMappingsClicked()); - this.dropMappingsItem.addActionListener(e -> this.gui.getController().dropMappings()); - this.reloadMappingsItem.addActionListener(e -> this.onReloadMappingsClicked()); - this.reloadAllItem.addActionListener(e -> this.onReloadAllClicked()); - this.exportSourceItem.addActionListener(e -> this.onExportSourceClicked()); - this.exportJarItem.addActionListener(e -> this.onExportJarClicked()); - this.statsItem.addActionListener(e -> StatsDialog.show(this.gui)); - this.configureKeyBindsItem.addActionListener(e -> ConfigureKeyBindsDialog.show(this.gui)); - this.exitItem.addActionListener(e -> this.gui.close()); - this.decompilerSettingsItem.addActionListener(e -> DecompilerSettingsDialog.show(this.gui)); - this.customScaleItem.addActionListener(e -> this.onCustomScaleClicked()); - this.fontItem.addActionListener(e -> this.onFontClicked(this.gui)); - this.searchItem.addActionListener(e -> this.onSearchClicked(false)); - this.searchAllItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.values())); - this.searchClassItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.CLASS)); - this.searchMethodItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.METHOD)); - this.searchFieldItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.FIELD)); - this.connectItem.addActionListener(e -> this.onConnectClicked()); - this.startServerItem.addActionListener(e -> this.onStartServerClicked()); - this.aboutItem.addActionListener(e -> AboutDialog.show(this.gui.getFrame())); - this.githubItem.addActionListener(e -> this.onGithubClicked()); - } - - public void setKeyBinds() { - this.saveMappingsItem.setAccelerator(KeyBinds.SAVE_MAPPINGS.toKeyStroke()); - this.dropMappingsItem.setAccelerator(KeyBinds.DROP_MAPPINGS.toKeyStroke()); - this.reloadMappingsItem.setAccelerator(KeyBinds.RELOAD_MAPPINGS.toKeyStroke()); - this.reloadAllItem.setAccelerator(KeyBinds.RELOAD_ALL.toKeyStroke()); - this.statsItem.setAccelerator(KeyBinds.MAPPING_STATS.toKeyStroke()); - this.searchItem.setAccelerator(KeyBinds.SEARCH.toKeyStroke()); - this.searchAllItem.setAccelerator(KeyBinds.SEARCH_ALL.toKeyStroke()); - this.searchClassItem.setAccelerator(KeyBinds.SEARCH_CLASS.toKeyStroke()); - this.searchMethodItem.setAccelerator(KeyBinds.SEARCH_METHOD.toKeyStroke()); - this.searchFieldItem.setAccelerator(KeyBinds.SEARCH_FIELD.toKeyStroke()); - } - - public void updateUiState() { - boolean jarOpen = this.gui.isJarOpen(); - ConnectionState connectionState = this.gui.getConnectionState(); - - this.connectItem.setEnabled(jarOpen && connectionState != ConnectionState.HOSTING); - this.connectItem.setText(I18n.translate(connectionState != ConnectionState.CONNECTED ? "menu.collab.connect" : "menu.collab.disconnect")); - this.startServerItem.setEnabled(jarOpen && connectionState != ConnectionState.CONNECTED); - this.startServerItem.setText(I18n.translate(connectionState != ConnectionState.HOSTING ? "menu.collab.server.start" : "menu.collab.server.stop")); - - this.jarCloseItem.setEnabled(jarOpen); - this.openMappingsItem.setEnabled(jarOpen); - this.saveMappingsItem.setEnabled(jarOpen && this.gui.mappingsFileChooser.getSelectedFile() != null && connectionState != ConnectionState.CONNECTED); - this.saveMappingsAsMenu.setEnabled(jarOpen); - this.closeMappingsItem.setEnabled(jarOpen); - this.reloadMappingsItem.setEnabled(jarOpen); - this.reloadAllItem.setEnabled(jarOpen); - this.exportSourceItem.setEnabled(jarOpen); - this.exportJarItem.setEnabled(jarOpen); - this.statsItem.setEnabled(jarOpen); - - this.devMenu.updateUiState(); - } - - public void retranslateUi() { - this.fileMenu.setText(I18n.translate("menu.file")); - this.jarOpenItem.setText(I18n.translate("menu.file.jar.open")); - this.jarCloseItem.setText(I18n.translate("menu.file.jar.close")); - this.openRecentMenu.setText(I18n.translate("menu.file.open_recent_project")); - this.maxRecentFilesItem.setText(I18n.translate("menu.file.max_recent_projects")); - this.openMappingsItem.setText(I18n.translate("menu.file.mappings.open")); - this.saveMappingsItem.setText(I18n.translate("menu.file.mappings.save")); - this.saveMappingsAsMenu.setText(I18n.translate("menu.file.mappings.save_as")); - this.closeMappingsItem.setText(I18n.translate("menu.file.mappings.close")); - this.dropMappingsItem.setText(I18n.translate("menu.file.mappings.drop")); - this.reloadMappingsItem.setText(I18n.translate("menu.file.reload_mappings")); - this.reloadAllItem.setText(I18n.translate("menu.file.reload_all")); - this.exportSourceItem.setText(I18n.translate("menu.file.export.source")); - this.exportJarItem.setText(I18n.translate("menu.file.export.jar")); - this.statsItem.setText(I18n.translate("menu.file.stats")); - this.configureKeyBindsItem.setText(I18n.translate("menu.file.configure_keybinds")); - this.crashHistoryMenu.setText(I18n.translate("menu.file.crash_history")); - this.exitItem.setText(I18n.translate("menu.file.exit")); - - this.decompilerMenu.setText(I18n.translate("menu.decompiler")); - this.decompilerSettingsItem.setText(I18n.translate("menu.decompiler.settings")); - - this.viewMenu.setText(I18n.translate("menu.view")); - this.themesMenu.setText(I18n.translate("menu.view.themes")); - this.notificationsMenu.setText(I18n.translate("menu.view.notifications")); - this.languagesMenu.setText(I18n.translate("menu.view.languages")); - this.scaleMenu.setText(I18n.translate("menu.view.scale")); - this.fontItem.setText(I18n.translate("menu.view.font")); - this.customScaleItem.setText(I18n.translate("menu.view.scale.custom")); - - this.searchMenu.setText(I18n.translate("menu.search")); - this.searchItem.setText(I18n.translate("menu.search")); - this.searchAllItem.setText(I18n.translate("menu.search.all")); - this.searchClassItem.setText(I18n.translate("menu.search.class")); - this.searchMethodItem.setText(I18n.translate("menu.search.method")); - this.searchFieldItem.setText(I18n.translate("menu.search.field")); - - this.collabMenu.setText(I18n.translate("menu.collab")); - this.connectItem.setText(I18n.translate("menu.collab.connect")); - this.startServerItem.setText(I18n.translate("menu.collab.server.start")); - - this.helpMenu.setText(I18n.translate("menu.help")); - this.aboutItem.setText(I18n.translate("menu.help.about")); - this.githubItem.setText(I18n.translate("menu.help.github")); - - this.devMenu.retranslateUi(); - } - - private void onOpenJarClicked() { - JFileChooser d = this.gui.jarFileChooser; - d.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); - d.setVisible(true); - int result = d.showOpenDialog(this.gui.getFrame()); - - if (result != JFileChooser.APPROVE_OPTION) { - return; - } - - File file = d.getSelectedFile(); - // checks if the file name is not empty - if (file != null) { - Path path = file.toPath(); - // checks if the file name corresponds to an existing file - if (Files.exists(path)) { - this.gui.getController().openJar(path); - } - - Config.main().stats.lastSelectedDir.setValue(d.getCurrentDirectory().getAbsolutePath(), true); - } - } - - private void onMaxRecentFilesClicked() { - String input = JOptionPane.showInputDialog(this.gui.getFrame(), I18n.translate("menu.file.dialog.max_recent_projects.set"), Config.main().maxRecentProjects.value()); - - if (input != null) { - try { - int max = Integer.parseInt(input); - if (max < 0) { - throw new NumberFormatException(); - } - - Config.main().maxRecentProjects.setValue(max, true); - } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(this.gui.getFrame(), I18n.translate("prompt.invalid_input"), I18n.translate("prompt.error"), JOptionPane.ERROR_MESSAGE); - } - } - } - - private void onSaveMappingsClicked() { - this.gui.getController().saveMappings(this.gui.mappingsFileChooser.getSelectedFile().toPath()); - } - - private void openMappingsDiscardPrompt(Runnable then) { - if (this.gui.getController().isDirty()) { - this.gui.showDiscardDiag((response -> { - if (response == JOptionPane.YES_OPTION) { - this.gui.saveMapping().thenRun(then); - } else if (response == JOptionPane.NO_OPTION) { - then.run(); - } - - return null; - }), I18n.translate("prompt.close.save"), I18n.translate("prompt.close.discard"), I18n.translate("prompt.cancel")); - } else { - then.run(); - } - } - - private void onCloseMappingsClicked() { - this.openMappingsDiscardPrompt(() -> this.gui.getController().closeMappings()); - } - - private void onReloadMappingsClicked() { - this.openMappingsDiscardPrompt(() -> this.gui.getController().reloadMappings()); - } - - private void onReloadAllClicked() { - this.openMappingsDiscardPrompt(() -> this.gui.getController().reloadAll()); - } - - private void onExportSourceClicked() { - this.gui.exportSourceFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); - if (this.gui.exportSourceFileChooser.showSaveDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { - Config.main().stats.lastSelectedDir.setValue(this.gui.exportSourceFileChooser.getCurrentDirectory().toString(), true); - this.gui.getController().exportSource(this.gui.exportSourceFileChooser.getSelectedFile().toPath()); - } - } - - private void onExportJarClicked() { - this.gui.exportJarFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); - this.gui.exportJarFileChooser.setVisible(true); - int result = this.gui.exportJarFileChooser.showSaveDialog(this.gui.getFrame()); - - if (result != JFileChooser.APPROVE_OPTION) { - return; - } - - if (this.gui.exportJarFileChooser.getSelectedFile() != null) { - Path path = this.gui.exportJarFileChooser.getSelectedFile().toPath(); - this.gui.getController().exportJar(path); - Config.main().stats.lastSelectedDir.setValue(this.gui.exportJarFileChooser.getCurrentDirectory().getAbsolutePath(), true); - } - } - - private void onCustomScaleClicked() { - String answer = (String) JOptionPane.showInputDialog(this.gui.getFrame(), I18n.translate("menu.view.scale.custom.title"), I18n.translate("menu.view.scale.custom.title"), - JOptionPane.QUESTION_MESSAGE, null, null, Double.toString(Config.main().scaleFactor.value() * 100)); - - if (answer == null) { - return; - } - - float newScale = 1.0f; - try { - newScale = Float.parseFloat(answer) / 100f; - } catch (NumberFormatException ignored) { - // ignored! - } - - ScaleUtil.setScaleFactor(newScale); - ChangeDialog.show(this.gui.getFrame()); - } - - private void onFontClicked(Gui gui) { - FontDialog.display(gui.getFrame()); - } - - private void onSearchClicked(boolean clear, SearchDialog.Type... types) { - if (this.gui.getController().getProject() != null) { - this.gui.getSearchDialog().show(clear, types); - } - } - - public void onConnectClicked() { - if (this.gui.getController().getClient() != null) { - this.gui.getController().disconnectIfConnected(null); - return; - } - - ConnectToServerDialog.Result result = ConnectToServerDialog.show(this.gui); - if (result == null) { - return; - } - - this.gui.getController().disconnectIfConnected(null); - try { - this.gui.getController().createClient(result.username(), result.address().address, result.address().port, result.password()); - if (Config.main().serverNotificationLevel.value() != NotificationManager.ServerNotificationLevel.NONE) { - this.gui.getNotificationManager().notify(new ParameterizedMessage(Message.CONNECTED_TO_SERVER, result.addressStr())); - } - - Config.net().username.setValue(result.username(), true); - Config.net().remoteAddress.setValue(result.addressStr(), true); - Config.net().password.setValue(String.valueOf(result.password()), true); - } catch (IOException e) { - JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.connect.error"), JOptionPane.ERROR_MESSAGE); - this.gui.getController().disconnectIfConnected(null); - } - - Arrays.fill(result.password(), (char) 0); - } - - public void onStartServerClicked() { - if (this.gui.getController().getServer() != null) { - this.gui.getController().disconnectIfConnected(null); - return; - } - - CreateServerDialog.Result result = CreateServerDialog.show(this.gui); - if (result == null) { - return; - } - - this.gui.getController().disconnectIfConnected(null); - try { - this.gui.getController().createServer(result.username(), result.port(), result.password()); - if (Config.main().serverNotificationLevel.value() != NotificationManager.ServerNotificationLevel.NONE) { - this.gui.getNotificationManager().notify(new ParameterizedMessage(Message.SERVER_STARTED, result.port())); - } - - Config.net().username.setValue(result.username(), true); - Config.net().serverPort.setValue(result.port(), true); - Config.net().serverPassword.setValue(String.valueOf(result.password()), true); - } catch (IOException e) { - JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.server.start.error"), JOptionPane.ERROR_MESSAGE); - this.gui.getController().disconnectIfConnected(null); - } - } - - private void onGithubClicked() { - GuiUtil.openUrl("https://github.com/QuiltMC/Enigma"); - } - - private void onOpenMappingsClicked() { - this.gui.mappingsFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); - this.gui.mappingsFileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); - - List types = this.gui.getController().getEnigma().getReadWriteServices().stream().filter(ReadWriteService::supportsReading).toList(); - ExtensionFileFilter.setupFileChooser(this.gui, this.gui.mappingsFileChooser, types.toArray(new ReadWriteService[0])); - - if (this.gui.mappingsFileChooser.showOpenDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { - File selectedFile = this.gui.mappingsFileChooser.getSelectedFile(); - Config.main().stats.lastSelectedDir.setValue(this.gui.mappingsFileChooser.getCurrentDirectory().toString(), true); - - Optional format = this.gui.getController().getEnigma().getReadWriteService(selectedFile.toPath()); - if (format.isPresent() && format.get().supportsReading()) { - this.gui.getController().openMappings(format.get(), selectedFile.toPath()); - } else { - String nonParseableMessage = I18n.translateFormatted("menu.file.open.non_parseable.unsupported_format", selectedFile); - if (format.isPresent()) { - nonParseableMessage = I18n.translateFormatted("menu.file.open.non_parseable", I18n.translate("mapping_format." + format.get().getId().split(":")[1].toLowerCase())); - } - - JOptionPane.showMessageDialog(this.gui.getFrame(), nonParseableMessage, I18n.translate("menu.file.open.cannot_open"), JOptionPane.ERROR_MESSAGE); - } - } - } - - public void reloadOpenRecentMenu(Gui gui) { - this.openRecentMenu.removeAll(); - List recentFilePairs = Config.main().recentProjects.value(); - - // find the longest common prefix among all mappings files - // this is to clear the "/home/user/wherever-you-store-your-mappings-projects/" part of the path and only show relevant information - Path prefix = null; - - if (recentFilePairs.size() > 1) { - List recentFiles = recentFilePairs.stream().map(Config.RecentProject::getMappingsPath).sorted().toList(); - prefix = recentFiles.get(0); - - for (int i = 1; i < recentFiles.size(); i++) { - if (prefix == null) { - break; - } - - prefix = findCommonPath(prefix, recentFiles.get(i)); - } - } - - for (Config.RecentProject recent : recentFilePairs) { - if (!Files.exists(recent.getJarPath()) || !Files.exists(recent.getMappingsPath())) { - continue; - } - - String jarName = recent.getJarPath().getFileName().toString(); - - // if there's no common prefix, just show the last directory in the tree - String mappingsName; - if (prefix != null) { - mappingsName = prefix.relativize(recent.getMappingsPath()).toString(); - } else { - mappingsName = recent.getMappingsPath().getFileName().toString(); - } - - JMenuItem item = new JMenuItem(jarName + " -> " + mappingsName); - item.addActionListener(event -> gui.getController().openJar(recent.getJarPath()).whenComplete((v, t) -> gui.getController().openMappings(recent.getMappingsPath()))); - this.openRecentMenu.add(item); - } - } - - /** - * Find the longest common path between two absolute(!!) paths. - */ - @Nullable - private static Path findCommonPath(Path a, Path b) { - int i = 0; - for (; i < Math.min(a.getNameCount(), b.getNameCount()); i++) { - Path nameA = a.getName(i); - Path nameB = b.getName(i); - - if (!nameA.equals(nameB)) { - break; - } - } - - return i != 0 ? a.getRoot().resolve(a.subpath(0, i)) : null; - } - - private static void prepareSaveMappingsAsMenu(JMenu saveMappingsAsMenu, JMenuItem saveMappingsItem, Gui gui) { - for (ReadWriteService format : gui.getController().getEnigma().getReadWriteServices()) { - if (format.supportsWriting()) { - JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.getId().toLowerCase(Locale.ROOT))); - item.addActionListener(event -> { - JFileChooser fileChooser = gui.mappingsFileChooser; - ExtensionFileFilter.setupFileChooser(gui, fileChooser, format); - - if (fileChooser.getCurrentDirectory() == null) { - fileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); - } - - if (fileChooser.showSaveDialog(gui.getFrame()) == JFileChooser.APPROVE_OPTION) { - Path savePath = ExtensionFileFilter.getSavePath(fileChooser); - gui.getController().saveMappings(savePath, format); - saveMappingsItem.setEnabled(true); - Config.main().stats.lastSelectedDir.setValue(fileChooser.getCurrentDirectory().toString()); - } - }); - saveMappingsAsMenu.add(item); - } - } - } - - private static void prepareDecompilerMenu(JMenu decompilerMenu, JMenuItem decompilerSettingsItem, Gui gui) { - ButtonGroup decompilerGroup = new ButtonGroup(); - - for (Decompiler decompiler : Decompiler.values()) { - JRadioButtonMenuItem decompilerButton = new JRadioButtonMenuItem(decompiler.name); - decompilerGroup.add(decompilerButton); - if (decompiler.equals(Config.decompiler().activeDecompiler.value())) { - decompilerButton.setSelected(true); - } - - decompilerButton.addActionListener(event -> { - gui.getController().setDecompiler(decompiler.service); - - Config.decompiler().activeDecompiler.setValue(decompiler, true); - }); - decompilerMenu.add(decompilerButton); - } - - decompilerMenu.addSeparator(); - decompilerMenu.add(decompilerSettingsItem); - } - - private static void prepareThemesMenu(JMenu themesMenu, Gui gui) { - ButtonGroup themeGroup = new ButtonGroup(); - for (Config.ThemeChoice themeChoice : Config.ThemeChoice.values()) { - JRadioButtonMenuItem themeButton = new JRadioButtonMenuItem(I18n.translate("menu.view.themes." + themeChoice.name().toLowerCase(Locale.ROOT))); - themeGroup.add(themeButton); - if (themeChoice.equals(Config.main().theme.value())) { - themeButton.setSelected(true); - } - - themeButton.addActionListener(e -> { - Config.main().theme.setValue(themeChoice, true); - ChangeDialog.show(gui.getFrame()); - }); - themesMenu.add(themeButton); - } - } - - private static void prepareLanguagesMenu(JMenu languagesMenu) { - ButtonGroup languageGroup = new ButtonGroup(); - for (String lang : I18n.getAvailableLanguages()) { - JRadioButtonMenuItem languageButton = new JRadioButtonMenuItem(I18n.getLanguageName(lang)); - languageGroup.add(languageButton); - if (lang.equals(Config.main().language.value())) { - languageButton.setSelected(true); - } - - languageButton.addActionListener(event -> { - Config.main().language.setValue(lang, true); - I18n.setLanguage(lang); - LanguageUtil.dispatchLanguageChange(); - }); - languagesMenu.add(languageButton); - } - } - - private static void prepareScaleMenu(JMenu scaleMenu, Gui gui) { - ButtonGroup scaleGroup = new ButtonGroup(); - Map scaleButtons = IntStream.of(100, 125, 150, 175, 200) - .mapToObj(scaleFactor -> { - float realScaleFactor = scaleFactor / 100f; - JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(String.format("%d%%", scaleFactor)); - menuItem.addActionListener(event -> ScaleUtil.setScaleFactor(realScaleFactor)); - menuItem.addActionListener(event -> ChangeDialog.show(gui.getFrame())); - scaleGroup.add(menuItem); - scaleMenu.add(menuItem); - return new Pair<>(realScaleFactor, menuItem); - }) - .collect(Collectors.toMap(Pair::a, Pair::b)); - - JRadioButtonMenuItem currentScaleButton = scaleButtons.get(Config.main().scaleFactor.value()); - if (currentScaleButton != null) { - currentScaleButton.setSelected(true); - } - - ScaleUtil.addListener((newScale, oldScale) -> { - JRadioButtonMenuItem mi = scaleButtons.get(newScale); - if (mi != null) { - mi.setSelected(true); - } else { - scaleGroup.clearSelection(); - } - }); - } - - private static void prepareNotificationsMenu(JMenu notificationsMenu) { - ButtonGroup notificationsGroup = new ButtonGroup(); - - for (NotificationManager.ServerNotificationLevel level : NotificationManager.ServerNotificationLevel.values()) { - JRadioButtonMenuItem notificationsButton = new JRadioButtonMenuItem(level.getText()); - notificationsGroup.add(notificationsButton); - - if (level.equals(Config.main().serverNotificationLevel.value())) { - notificationsButton.setSelected(true); - } - - notificationsButton.addActionListener(event -> Config.main().serverNotificationLevel.setValue(level, true)); - - notificationsMenu.add(notificationsButton); - } - } - - public void prepareCrashHistoryMenu() { - this.crashHistoryMenu.removeAll(); - ButtonGroup crashHistoryGroup = new ButtonGroup(); - - for (int i = 0; i < this.gui.getCrashHistory().size(); i++) { - Throwable t = this.gui.getCrashHistory().get(i); - JMenuItem crashHistoryButton = new JMenuItem(i + " - " + t.toString()); - crashHistoryGroup.add(crashHistoryButton); - - crashHistoryButton.addActionListener(event -> CrashDialog.show(t, false)); - - this.crashHistoryMenu.add(crashHistoryButton); - } - - this.crashHistoryMenu.setEnabled(!this.gui.getCrashHistory().isEmpty()); - } -} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/TooltipPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/StatsTooltipPanel.java similarity index 78% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/TooltipPanel.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/StatsTooltipPanel.java index 1ddd5803c..6ed207131 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/TooltipPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/StatsTooltipPanel.java @@ -4,16 +4,17 @@ import org.quiltmc.enigma.api.stats.StatType; import org.quiltmc.enigma.api.stats.StatsGenerator; import org.quiltmc.enigma.api.stats.StatsResult; +import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.util.I18n; import javax.swing.JPanel; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; -public abstract class TooltipPanel extends JPanel { +public abstract class StatsTooltipPanel extends JPanel { private final GuiController controller; - public TooltipPanel(GuiController controller) { + public StatsTooltipPanel(GuiController controller) { this.controller = controller; } @@ -32,7 +33,9 @@ public String getToolTipText(MouseEvent event) { if ((event.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0) { for (int i = 0; i < StatType.values().length; i++) { StatType type = StatType.values()[i]; - text.append(type.getName()).append(": ").append(stats.toString(type)).append(i == StatType.values().length - 1 ? "" : "\n"); + if (Config.stats().includedStatTypes.value().contains(type)) { + text.append(type.getName()).append(": ").append(stats.toString(type)).append(i == StatType.values().length - 1 ? "" : "\n"); + } } } else { text.append(stats); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java new file mode 100644 index 000000000..92831efaa --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java @@ -0,0 +1,6 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import javax.swing.JMenu; + +public class AbstractEnigmaMenu extends JMenu implements EnigmaMenu { +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java new file mode 100644 index 000000000..62fd35f35 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/CollabMenu.java @@ -0,0 +1,108 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.gui.ConnectionState; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.NotificationManager; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.dialog.ConnectToServerDialog; +import org.quiltmc.enigma.gui.dialog.CreateServerDialog; +import org.quiltmc.enigma.util.I18n; +import org.quiltmc.enigma.util.validation.Message; +import org.quiltmc.enigma.util.validation.ParameterizedMessage; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import java.io.IOException; +import java.util.Arrays; + +public class CollabMenu extends AbstractEnigmaMenu { + private final Gui gui; + + private final JMenuItem connectItem = new JMenuItem(); + private final JMenuItem startServerItem = new JMenuItem(); + + public CollabMenu(Gui gui) { + this.gui = gui; + + this.add(this.connectItem); + this.add(this.startServerItem); + + this.connectItem.addActionListener(e -> this.onConnectClicked()); + this.startServerItem.addActionListener(e -> this.onStartServerClicked()); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.collab")); + this.connectItem.setText(I18n.translate("menu.collab.connect")); + this.startServerItem.setText(I18n.translate("menu.collab.server.start")); + } + + @Override + public void updateState() { + boolean jarOpen = this.gui.isJarOpen(); + ConnectionState connectionState = this.gui.getConnectionState(); + + this.connectItem.setEnabled(jarOpen && connectionState != ConnectionState.HOSTING); + this.connectItem.setText(I18n.translate(connectionState != ConnectionState.CONNECTED ? "menu.collab.connect" : "menu.collab.disconnect")); + this.startServerItem.setEnabled(jarOpen && connectionState != ConnectionState.CONNECTED); + this.startServerItem.setText(I18n.translate(connectionState != ConnectionState.HOSTING ? "menu.collab.server.start" : "menu.collab.server.stop")); + + } + + public void onConnectClicked() { + if (this.gui.getController().getClient() != null) { + this.gui.getController().disconnectIfConnected(null); + return; + } + + ConnectToServerDialog.Result result = ConnectToServerDialog.show(this.gui); + if (result == null) { + return; + } + + this.gui.getController().disconnectIfConnected(null); + try { + this.gui.getController().createClient(result.username(), result.address().address, result.address().port, result.password()); + if (Config.main().serverNotificationLevel.value() != NotificationManager.ServerNotificationLevel.NONE) { + this.gui.getNotificationManager().notify(new ParameterizedMessage(Message.CONNECTED_TO_SERVER, result.addressStr())); + } + + Config.net().username.setValue(result.username(), true); + Config.net().remoteAddress.setValue(result.addressStr(), true); + Config.net().password.setValue(String.valueOf(result.password()), true); + } catch (IOException e) { + JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.connect.error"), JOptionPane.ERROR_MESSAGE); + this.gui.getController().disconnectIfConnected(null); + } + + Arrays.fill(result.password(), (char) 0); + } + + public void onStartServerClicked() { + if (this.gui.getController().getServer() != null) { + this.gui.getController().disconnectIfConnected(null); + return; + } + + CreateServerDialog.Result result = CreateServerDialog.show(this.gui); + if (result == null) { + return; + } + + this.gui.getController().disconnectIfConnected(null); + try { + this.gui.getController().createServer(result.username(), result.port(), result.password()); + if (Config.main().serverNotificationLevel.value() != NotificationManager.ServerNotificationLevel.NONE) { + this.gui.getNotificationManager().notify(new ParameterizedMessage(Message.SERVER_STARTED, result.port())); + } + + Config.net().username.setValue(result.username(), true); + Config.net().serverPort.setValue(result.port(), true); + Config.net().serverPassword.setValue(String.valueOf(result.password()), true); + } catch (IOException e) { + JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.server.start.error"), JOptionPane.ERROR_MESSAGE); + this.gui.getController().disconnectIfConnected(null); + } + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java new file mode 100644 index 000000000..31b9c336b --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DecompilerMenu.java @@ -0,0 +1,49 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.config.Decompiler; +import org.quiltmc.enigma.gui.dialog.decompiler.DecompilerSettingsDialog; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.ButtonGroup; +import javax.swing.JMenuItem; +import javax.swing.JRadioButtonMenuItem; + +public class DecompilerMenu extends AbstractEnigmaMenu { + private final Gui gui; + + private final JMenuItem decompilerSettingsItem = new JMenuItem(); + + public DecompilerMenu(Gui gui) { + this.gui = gui; + + ButtonGroup decompilerGroup = new ButtonGroup(); + + for (Decompiler decompiler : Decompiler.values()) { + JRadioButtonMenuItem decompilerButton = new JRadioButtonMenuItem(decompiler.name); + decompilerGroup.add(decompilerButton); + if (decompiler.equals(Config.decompiler().activeDecompiler.value())) { + decompilerButton.setSelected(true); + } + + decompilerButton.addActionListener(event -> { + this.gui.getController().setDecompiler(decompiler.service); + + Config.decompiler().activeDecompiler.setValue(decompiler, true); + }); + this.add(decompilerButton); + } + + this.addSeparator(); + this.add(this.decompilerSettingsItem); + + this.decompilerSettingsItem.addActionListener(e -> DecompilerSettingsDialog.show(this.gui)); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.decompiler")); + this.decompilerSettingsItem.setText(I18n.translate("menu.decompiler.settings")); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/DevMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java similarity index 96% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/DevMenu.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java index 5b0ef0541..82d43febb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/DevMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/DevMenu.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.gui.element; +package org.quiltmc.enigma.gui.element.menu_bar; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; @@ -12,7 +12,6 @@ import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; import javax.swing.JFrame; -import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; @@ -26,7 +25,7 @@ import java.io.StringWriter; import java.nio.file.Files; -public class DevMenu extends JMenu { +public class DevMenu extends AbstractEnigmaMenu { private final Gui gui; private final JCheckBoxMenuItem showMappingSourcePluginItem = new JCheckBoxMenuItem(); @@ -48,7 +47,8 @@ public DevMenu(Gui gui) { this.printMappingTreeItem.addActionListener(e -> this.onPrintMappingTreeClicked()); } - public void retranslateUi() { + @Override + public void retranslate() { this.setText("Dev"); this.showMappingSourcePluginItem.setText(I18n.translate("dev.menu.show_mapping_source_plugin")); @@ -57,7 +57,8 @@ public void retranslateUi() { this.printMappingTreeItem.setText(I18n.translate("dev.menu.print_mapping_tree")); } - public void updateUiState() { + @Override + public void updateState() { boolean jarOpen = this.gui.isJarOpen(); this.printMappingTreeItem.setEnabled(jarOpen); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java new file mode 100644 index 000000000..ffdce2d3f --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/EnigmaMenu.java @@ -0,0 +1,9 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +public interface EnigmaMenu { + default void setKeyBinds() {} + + default void updateState() {} + + default void retranslate() {} +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/FileMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/FileMenu.java new file mode 100644 index 000000000..0dc6f2207 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/FileMenu.java @@ -0,0 +1,365 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.gui.ConnectionState; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.config.keybind.KeyBinds; +import org.quiltmc.enigma.gui.dialog.CrashDialog; +import org.quiltmc.enigma.gui.dialog.StatsDialog; +import org.quiltmc.enigma.gui.dialog.keybind.ConfigureKeyBindsDialog; +import org.quiltmc.enigma.gui.util.ExtensionFileFilter; +import org.quiltmc.enigma.util.I18n; + +import javax.annotation.Nullable; +import javax.swing.ButtonGroup; +import javax.swing.JFileChooser; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +public class FileMenu extends AbstractEnigmaMenu { + private final Gui gui; + + private final JMenuItem jarOpenItem = new JMenuItem(); + private final JMenuItem jarCloseItem = new JMenuItem(); + private final JMenuItem openMappingsItem = new JMenuItem(); + private final JMenu openRecentMenu = new JMenu(); + private final JMenuItem maxRecentFilesItem = new JMenuItem(); + private final JMenuItem saveMappingsItem = new JMenuItem(); + private final JMenu saveMappingsAsMenu = new JMenu(); + private final JMenuItem closeMappingsItem = new JMenuItem(); + private final JMenuItem dropMappingsItem = new JMenuItem(); + private final JMenuItem reloadMappingsItem = new JMenuItem(); + private final JMenuItem reloadAllItem = new JMenuItem(); + private final JMenuItem exportSourceItem = new JMenuItem(); + private final JMenuItem exportJarItem = new JMenuItem(); + private final JMenuItem statsItem = new JMenuItem(); + private final JMenuItem configureKeyBindsItem = new JMenuItem(); + private final JMenuItem exitItem = new JMenuItem(); + private final JMenu crashHistoryMenu = new JMenu(); + + public FileMenu(Gui gui) { + this.gui = gui; + + this.reloadOpenRecentMenu(); + this.prepareSaveMappingsAsMenu(); + this.prepareCrashHistoryMenu(); + + this.add(this.jarOpenItem); + this.add(this.jarCloseItem); + this.addSeparator(); + this.add(this.openRecentMenu); + this.add(this.maxRecentFilesItem); + this.addSeparator(); + this.add(this.openMappingsItem); + this.add(this.saveMappingsItem); + this.add(this.saveMappingsAsMenu); + this.add(this.closeMappingsItem); + this.add(this.dropMappingsItem); + this.addSeparator(); + this.add(this.reloadMappingsItem); + this.add(this.reloadAllItem); + this.addSeparator(); + this.add(this.exportSourceItem); + this.add(this.exportJarItem); + this.addSeparator(); + this.add(this.statsItem); + this.addSeparator(); + this.add(this.configureKeyBindsItem); + this.addSeparator(); + this.add(this.crashHistoryMenu); + this.add(this.exitItem); + + this.jarOpenItem.addActionListener(e -> this.onOpenJarClicked()); + this.openMappingsItem.addActionListener(e -> this.onOpenMappingsClicked()); + this.jarCloseItem.addActionListener(e -> this.gui.getController().closeJar()); + this.maxRecentFilesItem.addActionListener(e -> this.onMaxRecentFilesClicked()); + this.saveMappingsItem.addActionListener(e -> this.onSaveMappingsClicked()); + this.closeMappingsItem.addActionListener(e -> this.onCloseMappingsClicked()); + this.dropMappingsItem.addActionListener(e -> this.gui.getController().dropMappings()); + this.reloadMappingsItem.addActionListener(e -> this.onReloadMappingsClicked()); + this.reloadAllItem.addActionListener(e -> this.onReloadAllClicked()); + this.exportSourceItem.addActionListener(e -> this.onExportSourceClicked()); + this.exportJarItem.addActionListener(e -> this.onExportJarClicked()); + this.statsItem.addActionListener(e -> StatsDialog.show(this.gui)); + this.configureKeyBindsItem.addActionListener(e -> ConfigureKeyBindsDialog.show(this.gui)); + this.exitItem.addActionListener(e -> this.gui.close()); + } + + @Override + public void setKeyBinds() { + this.saveMappingsItem.setAccelerator(KeyBinds.SAVE_MAPPINGS.toKeyStroke()); + this.dropMappingsItem.setAccelerator(KeyBinds.DROP_MAPPINGS.toKeyStroke()); + this.reloadMappingsItem.setAccelerator(KeyBinds.RELOAD_MAPPINGS.toKeyStroke()); + this.reloadAllItem.setAccelerator(KeyBinds.RELOAD_ALL.toKeyStroke()); + this.statsItem.setAccelerator(KeyBinds.MAPPING_STATS.toKeyStroke()); + } + + @Override + public void updateState() { + boolean jarOpen = this.gui.isJarOpen(); + + this.jarCloseItem.setEnabled(jarOpen); + this.openMappingsItem.setEnabled(jarOpen); + this.saveMappingsItem.setEnabled(jarOpen && this.gui.mappingsFileChooser.getSelectedFile() != null && this.gui.getConnectionState() != ConnectionState.CONNECTED); + this.saveMappingsAsMenu.setEnabled(jarOpen); + this.closeMappingsItem.setEnabled(jarOpen); + this.reloadMappingsItem.setEnabled(jarOpen); + this.reloadAllItem.setEnabled(jarOpen); + this.exportSourceItem.setEnabled(jarOpen); + this.exportJarItem.setEnabled(jarOpen); + this.statsItem.setEnabled(jarOpen); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.file")); + this.jarOpenItem.setText(I18n.translate("menu.file.jar.open")); + this.jarCloseItem.setText(I18n.translate("menu.file.jar.close")); + this.openRecentMenu.setText(I18n.translate("menu.file.open_recent_project")); + this.maxRecentFilesItem.setText(I18n.translate("menu.file.max_recent_projects")); + this.openMappingsItem.setText(I18n.translate("menu.file.mappings.open")); + this.saveMappingsItem.setText(I18n.translate("menu.file.mappings.save")); + this.saveMappingsAsMenu.setText(I18n.translate("menu.file.mappings.save_as")); + this.closeMappingsItem.setText(I18n.translate("menu.file.mappings.close")); + this.dropMappingsItem.setText(I18n.translate("menu.file.mappings.drop")); + this.reloadMappingsItem.setText(I18n.translate("menu.file.reload_mappings")); + this.reloadAllItem.setText(I18n.translate("menu.file.reload_all")); + this.exportSourceItem.setText(I18n.translate("menu.file.export.source")); + this.exportJarItem.setText(I18n.translate("menu.file.export.jar")); + this.statsItem.setText(I18n.translate("menu.file.stats")); + this.configureKeyBindsItem.setText(I18n.translate("menu.file.configure_keybinds")); + this.crashHistoryMenu.setText(I18n.translate("menu.file.crash_history")); + this.exitItem.setText(I18n.translate("menu.file.exit")); + } + + public void reloadOpenRecentMenu() { + this.openRecentMenu.removeAll(); + List recentFilePairs = Config.main().recentProjects.value(); + + // find the longest common prefix among all mappings files + // this is to clear the "/home/user/wherever-you-store-your-mappings-projects/" part of the path and only show relevant information + Path prefix = null; + + if (recentFilePairs.size() > 1) { + List recentFiles = recentFilePairs.stream().map(Config.RecentProject::getMappingsPath).sorted().toList(); + prefix = recentFiles.get(0); + + for (int i = 1; i < recentFiles.size(); i++) { + if (prefix == null) { + break; + } + + prefix = findCommonPath(prefix, recentFiles.get(i)); + } + } + + for (Config.RecentProject recent : recentFilePairs) { + if (!Files.exists(recent.getJarPath()) || !Files.exists(recent.getMappingsPath())) { + continue; + } + + String jarName = recent.getJarPath().getFileName().toString(); + + // if there's no common prefix, just show the last directory in the tree + String mappingsName; + if (prefix != null) { + mappingsName = prefix.relativize(recent.getMappingsPath()).toString(); + } else { + mappingsName = recent.getMappingsPath().getFileName().toString(); + } + + JMenuItem item = new JMenuItem(jarName + " -> " + mappingsName); + item.addActionListener(event -> this.gui.getController().openJar(recent.getJarPath()).whenComplete((v, t) -> this.gui.getController().openMappings(recent.getMappingsPath()))); + this.openRecentMenu.add(item); + } + } + + /** + * Find the longest common path between two absolute(!!) paths. + */ + @Nullable + private static Path findCommonPath(Path a, Path b) { + int i = 0; + for (; i < Math.min(a.getNameCount(), b.getNameCount()); i++) { + Path nameA = a.getName(i); + Path nameB = b.getName(i); + + if (!nameA.equals(nameB)) { + break; + } + } + + return i != 0 ? a.getRoot().resolve(a.subpath(0, i)) : null; + } + + private void prepareSaveMappingsAsMenu() { + for (ReadWriteService format : this.gui.getController().getEnigma().getReadWriteServices()) { + if (format.supportsWriting()) { + JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.getId().toLowerCase(Locale.ROOT))); + item.addActionListener(event -> { + JFileChooser fileChooser = this.gui.mappingsFileChooser; + ExtensionFileFilter.setupFileChooser(this.gui, fileChooser, format); + + if (fileChooser.getCurrentDirectory() == null) { + fileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); + } + + if (fileChooser.showSaveDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + Path savePath = ExtensionFileFilter.getSavePath(fileChooser); + this.gui.getController().saveMappings(savePath, format); + this.saveMappingsItem.setEnabled(true); + Config.main().stats.lastSelectedDir.setValue(fileChooser.getCurrentDirectory().toString()); + } + }); + + this.saveMappingsAsMenu.add(item); + } + } + } + + public void prepareCrashHistoryMenu() { + this.crashHistoryMenu.removeAll(); + ButtonGroup crashHistoryGroup = new ButtonGroup(); + + for (int i = 0; i < this.gui.getCrashHistory().size(); i++) { + Throwable t = this.gui.getCrashHistory().get(i); + JMenuItem crashHistoryButton = new JMenuItem(i + " - " + t.toString()); + crashHistoryGroup.add(crashHistoryButton); + + crashHistoryButton.addActionListener(event -> CrashDialog.show(t, false)); + + this.crashHistoryMenu.add(crashHistoryButton); + } + + this.crashHistoryMenu.setEnabled(!this.gui.getCrashHistory().isEmpty()); + } + + private void onOpenJarClicked() { + JFileChooser d = this.gui.jarFileChooser; + d.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); + d.setVisible(true); + int result = d.showOpenDialog(this.gui.getFrame()); + + if (result != JFileChooser.APPROVE_OPTION) { + return; + } + + File file = d.getSelectedFile(); + // checks if the file name is not empty + if (file != null) { + Path path = file.toPath(); + // checks if the file name corresponds to an existing file + if (Files.exists(path)) { + this.gui.getController().openJar(path); + } + + Config.main().stats.lastSelectedDir.setValue(d.getCurrentDirectory().getAbsolutePath(), true); + } + } + + private void onOpenMappingsClicked() { + this.gui.mappingsFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); + this.gui.mappingsFileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + + List types = this.gui.getController().getEnigma().getReadWriteServices().stream().filter(ReadWriteService::supportsReading).toList(); + ExtensionFileFilter.setupFileChooser(this.gui, this.gui.mappingsFileChooser, types.toArray(new ReadWriteService[0])); + + if (this.gui.mappingsFileChooser.showOpenDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + File selectedFile = this.gui.mappingsFileChooser.getSelectedFile(); + Config.main().stats.lastSelectedDir.setValue(this.gui.mappingsFileChooser.getCurrentDirectory().toString(), true); + + Optional format = this.gui.getController().getEnigma().getReadWriteService(selectedFile.toPath()); + if (format.isPresent() && format.get().supportsReading()) { + this.gui.getController().openMappings(format.get(), selectedFile.toPath()); + } else { + String nonParseableMessage = I18n.translateFormatted("menu.file.open.non_parseable.unsupported_format", selectedFile); + if (format.isPresent()) { + nonParseableMessage = I18n.translateFormatted("menu.file.open.non_parseable", I18n.translate("mapping_format." + format.get().getId().split(":")[1].toLowerCase())); + } + + JOptionPane.showMessageDialog(this.gui.getFrame(), nonParseableMessage, I18n.translate("menu.file.open.cannot_open"), JOptionPane.ERROR_MESSAGE); + } + } + } + + private void onMaxRecentFilesClicked() { + String input = JOptionPane.showInputDialog(this.gui.getFrame(), I18n.translate("menu.file.dialog.max_recent_projects.set"), Config.main().maxRecentProjects.value()); + + if (input != null) { + try { + int max = Integer.parseInt(input); + if (max < 0) { + throw new NumberFormatException(); + } + + Config.main().maxRecentProjects.setValue(max, true); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this.gui.getFrame(), I18n.translate("prompt.invalid_input"), I18n.translate("prompt.error"), JOptionPane.ERROR_MESSAGE); + } + } + } + + private void onSaveMappingsClicked() { + this.gui.getController().saveMappings(this.gui.mappingsFileChooser.getSelectedFile().toPath()); + } + + private void openMappingsDiscardPrompt(Runnable then) { + if (this.gui.getController().isDirty()) { + this.gui.showDiscardDiag((response -> { + if (response == JOptionPane.YES_OPTION) { + this.gui.saveMapping().thenRun(then); + } else if (response == JOptionPane.NO_OPTION) { + then.run(); + } + + return null; + }), I18n.translate("prompt.close.save"), I18n.translate("prompt.close.discard"), I18n.translate("prompt.cancel")); + } else { + then.run(); + } + } + + private void onCloseMappingsClicked() { + this.openMappingsDiscardPrompt(() -> this.gui.getController().closeMappings()); + } + + private void onReloadMappingsClicked() { + this.openMappingsDiscardPrompt(() -> this.gui.getController().reloadMappings()); + } + + private void onReloadAllClicked() { + this.openMappingsDiscardPrompt(() -> this.gui.getController().reloadAll()); + } + + private void onExportSourceClicked() { + this.gui.exportSourceFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); + if (this.gui.exportSourceFileChooser.showSaveDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + Config.main().stats.lastSelectedDir.setValue(this.gui.exportSourceFileChooser.getCurrentDirectory().toString(), true); + this.gui.getController().exportSource(this.gui.exportSourceFileChooser.getSelectedFile().toPath()); + } + } + + private void onExportJarClicked() { + this.gui.exportJarFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); + this.gui.exportJarFileChooser.setVisible(true); + int result = this.gui.exportJarFileChooser.showSaveDialog(this.gui.getFrame()); + + if (result != JFileChooser.APPROVE_OPTION) { + return; + } + + if (this.gui.exportJarFileChooser.getSelectedFile() != null) { + Path path = this.gui.exportJarFileChooser.getSelectedFile().toPath(); + this.gui.getController().exportJar(path); + Config.main().stats.lastSelectedDir.setValue(this.gui.exportJarFileChooser.getCurrentDirectory().getAbsolutePath(), true); + } + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java new file mode 100644 index 000000000..a775081cd --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/HelpMenu.java @@ -0,0 +1,36 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.dialog.AboutDialog; +import org.quiltmc.enigma.gui.util.GuiUtil; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JMenuItem; + +public class HelpMenu extends AbstractEnigmaMenu { + private final Gui gui; + + private final JMenuItem aboutItem = new JMenuItem(); + private final JMenuItem githubItem = new JMenuItem(); + + public HelpMenu(Gui gui) { + this.gui = gui; + + this.add(this.aboutItem); + this.add(this.githubItem); + + this.aboutItem.addActionListener(e -> AboutDialog.show(this.gui.getFrame())); + this.githubItem.addActionListener(e -> this.onGithubClicked()); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.help")); + this.aboutItem.setText(I18n.translate("menu.help.about")); + this.githubItem.setText(I18n.translate("menu.help.github")); + } + + private void onGithubClicked() { + GuiUtil.openUrl("https://github.com/QuiltMC/Enigma"); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java new file mode 100644 index 000000000..6f71928dd --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/MenuBar.java @@ -0,0 +1,75 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; + +import java.util.ArrayList; +import java.util.List; + +public class MenuBar { + private final List menus = new ArrayList<>(); + + private final CollabMenu collabMenu; + private final FileMenu fileMenu; + + private final Gui gui; + + public MenuBar(Gui gui) { + this.gui = gui; + + this.fileMenu = new FileMenu(gui); + DecompilerMenu decompilerMenu = new DecompilerMenu(gui); + ViewMenu viewMenu = new ViewMenu(gui); + SearchMenu searchMenu = new SearchMenu(gui); + this.collabMenu = new CollabMenu(gui); + HelpMenu helpMenu = new HelpMenu(gui); + // Enabled with system property "enigma.development" or "--development" flag + DevMenu devMenu = new DevMenu(gui); + + this.retranslateUi(); + + this.addMenu(this.fileMenu); + this.addMenu(decompilerMenu); + this.addMenu(viewMenu); + this.addMenu(searchMenu); + this.addMenu(this.collabMenu); + this.addMenu(helpMenu); + + if (System.getProperty("enigma.development", "false").equalsIgnoreCase("true") || Config.main().development.anyEnabled) { + this.addMenu(devMenu); + } + + this.setKeyBinds(); + } + + private void addMenu(AbstractEnigmaMenu menu) { + this.gui.getMainWindow().getMenuBar().add(menu); + this.menus.add(menu); + } + + public void setKeyBinds() { + for (EnigmaMenu menu : this.menus) { + menu.setKeyBinds(); + } + } + + public void updateUiState() { + for (EnigmaMenu menu : this.menus) { + menu.updateState(); + } + } + + public void retranslateUi() { + for (EnigmaMenu menu : this.menus) { + menu.retranslate(); + } + } + + public CollabMenu getCollabMenu() { + return this.collabMenu; + } + + public FileMenu getFileMenu() { + return this.fileMenu; + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java new file mode 100644 index 000000000..a45387423 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/SearchMenu.java @@ -0,0 +1,60 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.keybind.KeyBinds; +import org.quiltmc.enigma.gui.dialog.SearchDialog; +import org.quiltmc.enigma.gui.util.GuiUtil; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JMenuItem; + +public class SearchMenu extends AbstractEnigmaMenu { + private final Gui gui; + + private final JMenuItem searchItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); + private final JMenuItem searchAllItem = new JMenuItem(GuiUtil.DEOBFUSCATED_ICON); + private final JMenuItem searchClassItem = new JMenuItem(GuiUtil.CLASS_ICON); + private final JMenuItem searchMethodItem = new JMenuItem(GuiUtil.METHOD_ICON); + private final JMenuItem searchFieldItem = new JMenuItem(GuiUtil.FIELD_ICON); + + public SearchMenu(Gui gui) { + this.gui = gui; + + this.add(this.searchItem); + this.add(this.searchAllItem); + this.add(this.searchClassItem); + this.add(this.searchMethodItem); + this.add(this.searchFieldItem); + + this.searchItem.addActionListener(e -> this.onSearchClicked(false)); + this.searchAllItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.values())); + this.searchClassItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.CLASS)); + this.searchMethodItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.METHOD)); + this.searchFieldItem.addActionListener(e -> this.onSearchClicked(true, SearchDialog.Type.FIELD)); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.search")); + this.searchItem.setText(I18n.translate("menu.search")); + this.searchAllItem.setText(I18n.translate("menu.search.all")); + this.searchClassItem.setText(I18n.translate("menu.search.class")); + this.searchMethodItem.setText(I18n.translate("menu.search.method")); + this.searchFieldItem.setText(I18n.translate("menu.search.field")); + } + + @Override + public void setKeyBinds() { + this.searchItem.setAccelerator(KeyBinds.SEARCH.toKeyStroke()); + this.searchAllItem.setAccelerator(KeyBinds.SEARCH_ALL.toKeyStroke()); + this.searchClassItem.setAccelerator(KeyBinds.SEARCH_CLASS.toKeyStroke()); + this.searchMethodItem.setAccelerator(KeyBinds.SEARCH_METHOD.toKeyStroke()); + this.searchFieldItem.setAccelerator(KeyBinds.SEARCH_FIELD.toKeyStroke()); + } + + private void onSearchClicked(boolean clear, SearchDialog.Type... types) { + if (this.gui.getController().getProject() != null) { + this.gui.getSearchDialog().show(clear, types); + } + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ViewMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ViewMenu.java new file mode 100644 index 000000000..779559617 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/ViewMenu.java @@ -0,0 +1,221 @@ +package org.quiltmc.enigma.gui.element.menu_bar; + +import org.quiltmc.enigma.api.stats.StatType; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.NotificationManager; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.dialog.ChangeDialog; +import org.quiltmc.enigma.gui.dialog.FontDialog; +import org.quiltmc.enigma.gui.util.LanguageUtil; +import org.quiltmc.enigma.gui.util.ScaleUtil; +import org.quiltmc.enigma.util.I18n; +import org.quiltmc.enigma.util.Pair; + +import javax.swing.ButtonGroup; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JRadioButtonMenuItem; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ViewMenu extends AbstractEnigmaMenu { + private final Gui gui; + + private final JMenu themesMenu = new JMenu(); + private final JMenu languagesMenu = new JMenu(); + private final JMenu scaleMenu = new JMenu(); + private final JMenu notificationsMenu = new JMenu(); + private final JMenu statIconsMenu = new JMenu(); + private final JMenuItem fontItem = new JMenuItem(); + private final JMenuItem customScaleItem = new JMenuItem(); + + public ViewMenu(Gui gui) { + this.gui = gui; + + this.prepareThemesMenu(); + this.prepareLanguagesMenu(); + this.prepareScaleMenu(); + this.prepareNotificationsMenu(); + this.prepareStatIconsMenu(); + + this.add(this.themesMenu); + this.add(this.languagesMenu); + this.add(this.notificationsMenu); + this.scaleMenu.add(this.customScaleItem); + this.add(this.scaleMenu); + this.add(this.statIconsMenu); + this.add(this.fontItem); + + this.customScaleItem.addActionListener(e -> this.onCustomScaleClicked()); + this.fontItem.addActionListener(e -> this.onFontClicked(this.gui)); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.view")); + this.themesMenu.setText(I18n.translate("menu.view.themes")); + this.notificationsMenu.setText(I18n.translate("menu.view.notifications")); + this.languagesMenu.setText(I18n.translate("menu.view.languages")); + this.scaleMenu.setText(I18n.translate("menu.view.scale")); + this.statIconsMenu.setText(I18n.translate("menu.view.stat_icons")); + this.fontItem.setText(I18n.translate("menu.view.font")); + this.customScaleItem.setText(I18n.translate("menu.view.scale.custom")); + } + + private void onCustomScaleClicked() { + String answer = (String) JOptionPane.showInputDialog(this.gui.getFrame(), I18n.translate("menu.view.scale.custom.title"), I18n.translate("menu.view.scale.custom.title"), + JOptionPane.QUESTION_MESSAGE, null, null, Double.toString(Config.main().scaleFactor.value() * 100)); + + if (answer == null) { + return; + } + + float newScale = 1.0f; + try { + newScale = Float.parseFloat(answer) / 100f; + } catch (NumberFormatException ignored) { + // ignored! + } + + ScaleUtil.setScaleFactor(newScale); + ChangeDialog.show(this.gui.getFrame()); + } + + private void onFontClicked(Gui gui) { + FontDialog.display(gui.getFrame()); + } + + private void prepareThemesMenu() { + ButtonGroup themeGroup = new ButtonGroup(); + for (Config.ThemeChoice themeChoice : Config.ThemeChoice.values()) { + JRadioButtonMenuItem themeButton = new JRadioButtonMenuItem(I18n.translate("menu.view.themes." + themeChoice.name().toLowerCase(Locale.ROOT))); + themeGroup.add(themeButton); + if (themeChoice.equals(Config.main().theme.value())) { + themeButton.setSelected(true); + } + + themeButton.addActionListener(e -> { + Config.main().theme.setValue(themeChoice, true); + ChangeDialog.show(this.gui.getFrame()); + }); + + this.themesMenu.add(themeButton); + } + } + + private void prepareLanguagesMenu() { + ButtonGroup languageGroup = new ButtonGroup(); + for (String lang : I18n.getAvailableLanguages()) { + JRadioButtonMenuItem languageButton = new JRadioButtonMenuItem(I18n.getLanguageName(lang)); + languageGroup.add(languageButton); + if (lang.equals(Config.main().language.value())) { + languageButton.setSelected(true); + } + + languageButton.addActionListener(event -> { + Config.main().language.setValue(lang, true); + I18n.setLanguage(lang); + LanguageUtil.dispatchLanguageChange(); + }); + + this.languagesMenu.add(languageButton); + } + } + + private void prepareScaleMenu() { + ButtonGroup scaleGroup = new ButtonGroup(); + Map scaleButtons = IntStream.of(100, 125, 150, 175, 200) + .mapToObj(scaleFactor -> { + float realScaleFactor = scaleFactor / 100f; + JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(String.format("%d%%", scaleFactor)); + menuItem.addActionListener(event -> ScaleUtil.setScaleFactor(realScaleFactor)); + menuItem.addActionListener(event -> ChangeDialog.show(this.gui.getFrame())); + scaleGroup.add(menuItem); + this.scaleMenu.add(menuItem); + return new Pair<>(realScaleFactor, menuItem); + }) + .collect(Collectors.toMap(Pair::a, Pair::b)); + + JRadioButtonMenuItem currentScaleButton = scaleButtons.get(Config.main().scaleFactor.value()); + if (currentScaleButton != null) { + currentScaleButton.setSelected(true); + } + + ScaleUtil.addListener((newScale, oldScale) -> { + JRadioButtonMenuItem mi = scaleButtons.get(newScale); + if (mi != null) { + mi.setSelected(true); + } else { + scaleGroup.clearSelection(); + } + }); + } + + private void prepareNotificationsMenu() { + ButtonGroup notificationsGroup = new ButtonGroup(); + + for (NotificationManager.ServerNotificationLevel level : NotificationManager.ServerNotificationLevel.values()) { + JRadioButtonMenuItem notificationsButton = new JRadioButtonMenuItem(level.getText()); + notificationsGroup.add(notificationsButton); + + if (level.equals(Config.main().serverNotificationLevel.value())) { + notificationsButton.setSelected(true); + } + + notificationsButton.addActionListener(event -> Config.main().serverNotificationLevel.setValue(level, true)); + + this.notificationsMenu.add(notificationsButton); + } + } + + private void prepareStatIconsMenu() { + JMenu statTypes = new JMenu(I18n.translate("menu.view.stat_icons.included_types")); + for (StatType statType : StatType.values()) { + JCheckBoxMenuItem checkbox = new JCheckBoxMenuItem(statType.getName()); + checkbox.setSelected(Config.main().stats.includedStatTypes.value().contains(statType)); + checkbox.addActionListener(event -> { + if (checkbox.isSelected() && !Config.stats().includedStatTypes.value().contains(statType)) { + Config.stats().includedStatTypes.value().add(statType); + } else { + Config.stats().includedStatTypes.value().remove(statType); + } + + ViewMenu.this.gui.getController().regenerateAndUpdateStatIcons(); + }); + + statTypes.add(checkbox); + } + + JCheckBoxMenuItem enableIcons = new JCheckBoxMenuItem(I18n.translate("menu.view.stat_icons.enable_icons")); + JCheckBoxMenuItem includeSynthetic = new JCheckBoxMenuItem(I18n.translate("menu.view.stat_icons.include_synthetic")); + JCheckBoxMenuItem countFallback = new JCheckBoxMenuItem(I18n.translate("menu.view.stat_icons.count_fallback")); + + enableIcons.setSelected(Config.main().features.enableClassTreeStatIcons.value()); + includeSynthetic.setSelected(Config.main().stats.shouldIncludeSyntheticParameters.value()); + countFallback.setSelected(Config.main().stats.shouldCountFallbackNames.value()); + + enableIcons.addActionListener(event -> { + Config.main().features.enableClassTreeStatIcons.setValue(enableIcons.isSelected()); + ViewMenu.this.gui.getController().regenerateAndUpdateStatIcons(); + }); + + includeSynthetic.addActionListener(event -> { + Config.main().stats.shouldIncludeSyntheticParameters.setValue(includeSynthetic.isSelected()); + ViewMenu.this.gui.getController().regenerateAndUpdateStatIcons(); + }); + + countFallback.addActionListener(event -> { + Config.main().stats.shouldCountFallbackNames.setValue(countFallback.isSelected()); + ViewMenu.this.gui.getController().regenerateAndUpdateStatIcons(); + }); + + this.statIconsMenu.add(enableIcons); + this.statIconsMenu.add(includeSynthetic); + this.statIconsMenu.add(countFallback); + this.statIconsMenu.add(statTypes); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java index 33607b9dd..f96c65b33 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.gui.node; import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.gui.ClassSelector; import org.quiltmc.enigma.gui.EditableType; import org.quiltmc.enigma.gui.Gui; @@ -48,9 +49,9 @@ public void reloadStats(Gui gui, ClassSelector selector, boolean updateIfPresent @Override protected ClassSelectorClassNode doInBackground() { if (generator.getResultNullable() == null && generator.getOverallProgress() == null) { - generator.generate(ProgressListener.createEmpty(), EditableType.toStatTypes(gui.getEditableTypes()), false); + generator.generate(ProgressListener.createEmpty(), new GenerationParameters(EditableType.toStatTypes(gui.getEditableTypes()))); } else if (updateIfPresent) { - generator.generate(ProgressListener.createEmpty(), EditableType.toStatTypes(gui.getEditableTypes()), ClassSelectorClassNode.this.getObfEntry(), false); + generator.generate(ProgressListener.createEmpty(), ClassSelectorClassNode.this.getObfEntry(), new GenerationParameters(EditableType.toStatTypes(gui.getEditableTypes()))); } return ClassSelectorClassNode.this; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java index 81f82d4a6..517bdb805 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java @@ -47,7 +47,7 @@ public interface NameProposalService extends EnigmaService { /** * Marks names proposed by this service as 'fallback' names. * Fallback names will be visually differentiated in frontend applications, and should be expected to be of lower quality than a typical proposed name. - * Fallback names will not count towards statistics. + * Unlike regular proposed names, fallback names will not count towards statistics by default. * * @return whether names from this service should be marked as fallback */ diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/stats/GenerationParameters.java b/enigma/src/main/java/org/quiltmc/enigma/api/stats/GenerationParameters.java new file mode 100644 index 000000000..025a735b0 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/stats/GenerationParameters.java @@ -0,0 +1,29 @@ +package org.quiltmc.enigma.api.stats; + +import org.quiltmc.enigma.api.service.NameProposalService; + +import java.util.EnumSet; +import java.util.Set; + +/** + * Defines the parameters to be used when generating statistics via a {@link StatsGenerator}. + * @param includedTypes the {@link StatType stat types} to include in the result + * @param includeSynthetic whether to include synthetic entries in the result + * @param countFallback whether to count {@link NameProposalService#isFallback() fallback-proposed} entries as mapped in the result + */ +public record GenerationParameters(Set includedTypes, boolean includeSynthetic, boolean countFallback) { + /** + * Creates a default set of parameters. + */ + public GenerationParameters() { + this(EnumSet.allOf(StatType.class), false, false); + } + + /** + * Creates a default set of parameters including the given types. + * @param types the types of entry to include + */ + public GenerationParameters(Set types) { + this(types, false, false); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java b/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java index 43756081d..442261b0c 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.api.stats; -import com.google.common.base.Preconditions; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; @@ -25,7 +24,6 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.EnumMap; -import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,6 +36,7 @@ public class StatsGenerator { private final EntryResolver entryResolver; private ProjectStatsResult result = null; + private GenerationParameters lastParameters = new GenerationParameters(); private ProgressListener overallListener; private CountDownLatch generationLatch = null; @@ -59,12 +58,13 @@ public ProjectStatsResult getResultNullable() { } /** - * Gets the latest generated stats, or generates them if not yet present. + * Gets the latest generated stats, or generates them if not available. + * Regenerates stats if parameters have changed. * @return the stats */ - public ProjectStatsResult getResult(Set includedTypes, boolean includeSynthetic) { - if (this.result == null) { - return this.generate(ProgressListener.createEmpty(), includedTypes, includeSynthetic); + public ProjectStatsResult getResult(GenerationParameters parameters) { + if (this.result == null || !this.lastParameters.equals(parameters)) { + return this.generate(ProgressListener.createEmpty(), parameters); } return this.result; @@ -82,35 +82,32 @@ public ProgressListener getOverallProgress() { /** * Generates stats for the current project. * @param progress a listener to update with current progress - * @param includedTypes the types of entry to include in the stats - * @param includeSynthetic whether to include synthetic methods + * @param parameters the parameters to use for generation * @return the generated {@link ProjectStatsResult} */ - public ProjectStatsResult generate(ProgressListener progress, Set includedTypes, boolean includeSynthetic) { - return this.generate(progress, includedTypes, null, includeSynthetic); + public ProjectStatsResult generate(ProgressListener progress, GenerationParameters parameters) { + return this.generate(progress, null, parameters); } /** * Generates stats for the current project or updates existing stats with the provided class. * Somewhat thread-safe: will only generate stats on one thread at a time, awaiting generation on all other threads if called in parallel. * @param progress a listener to update with current progress - * @param includedTypes the types of entry to include in the stats * @param classEntry if stats are being generated for a single class, provide the class here - * @param includeSynthetic whether to include synthetic methods + * @param parameters the parameters to use for generation * @return the generated {@link ProjectStatsResult} for the provided class or package */ - public ProjectStatsResult generate(ProgressListener progress, Set includedTypes, @Nullable ClassEntry classEntry, boolean includeSynthetic) { + public ProjectStatsResult generate(ProgressListener progress, @Nullable ClassEntry classEntry, GenerationParameters parameters) { if (classEntry == null && this.overallListener == null) { this.overallListener = progress; } this.rebuildCache(); - includedTypes = EnumSet.copyOf(includedTypes); Map stats = this.result == null ? new HashMap<>() : this.result.getStats(); - if (this.result == null || classEntry == null) { - if (this.generationLatch == null) { + if (classEntry == null) { + if (this.generationLatch == null || this.generationLatch.getCount() == 0) { this.generationLatch = new CountDownLatch(1); List classes = this.entryIndex.getClasses() @@ -121,11 +118,12 @@ public ProjectStatsResult generate(ProgressListener progress, Set incl for (ClassEntry entry : classes) { progress.step(done++, I18n.translateFormatted("progress.stats.for", entry.getName())); - StatsResult result = this.generate(includedTypes, entry, includeSynthetic, false); + StatsResult result = this.generate(entry, parameters, false); stats.put(entry, result); } this.result = new ProjectStatsResult(this.project, stats); + this.lastParameters = parameters; this.generationLatch.countDown(); } else { try { @@ -136,9 +134,9 @@ public ProjectStatsResult generate(ProgressListener progress, Set incl } } } else { - Preconditions.checkNotNull(classEntry, "Entry cannot be null after initial stat generation!"); - stats.put(classEntry, this.generate(includedTypes, classEntry, includeSynthetic, false)); + stats.put(classEntry, this.generate(classEntry, parameters, false)); this.result = new ProjectStatsResult(this.project, stats); + this.lastParameters = parameters; } this.overallListener = null; @@ -161,16 +159,15 @@ private void addChildrenRecursively(List> entries, Entry toCheck) { /** * Generates stats for the provided class. - * @param includedTypes the types of entry to include in the stats * @param classEntry the class to generate stats for - * @param includeSynthetic whether to include synthetic parameters + * @param parameters the parameters to use for generation * @return the generated {@link StatsResult} */ - public StatsResult generate(Set includedTypes, ClassEntry classEntry, boolean includeSynthetic) { - return this.generate(includedTypes, classEntry, includeSynthetic, true); + public StatsResult generate(ClassEntry classEntry, GenerationParameters parameters) { + return this.generate(classEntry, parameters, true); } - private StatsResult generate(Set includedTypes, ClassEntry classEntry, boolean includeSynthetic, boolean rebuildCache) { + private StatsResult generate(ClassEntry classEntry, GenerationParameters parameters, boolean rebuildCache) { if (rebuildCache) { this.rebuildCache(); } @@ -187,10 +184,12 @@ private StatsResult generate(Set includedTypes, ClassEntry classEntry, entries.add(classEntry); + Set includedTypes = parameters.includedTypes(); + for (Entry entry : entries) { if (entry instanceof FieldEntry field && includedTypes.contains(StatType.FIELDS)) { if (!((FieldDefEntry) field).getAccess().isSynthetic()) { - this.update(StatType.FIELDS, mappableCounts, unmappedCounts, field); + this.update(StatType.FIELDS, mappableCounts, unmappedCounts, field, parameters); } } else if (entry instanceof MethodEntry method) { MethodEntry root = this.entryResolver @@ -201,11 +200,11 @@ private StatsResult generate(Set includedTypes, ClassEntry classEntry, if (root == method) { if (includedTypes.contains(StatType.METHODS) && !((MethodDefEntry) method).getAccess().isSynthetic()) { - this.update(StatType.METHODS, mappableCounts, unmappedCounts, method); + this.update(StatType.METHODS, mappableCounts, unmappedCounts, method, parameters); } ClassEntry containingClass = method.getContainingClass(); - if (includedTypes.contains(StatType.PARAMETERS) && !this.project.isAnonymousOrLocal(containingClass) && !(((MethodDefEntry) method).getAccess().isSynthetic() && !includeSynthetic)) { + if (includedTypes.contains(StatType.PARAMETERS) && !this.project.isAnonymousOrLocal(containingClass) && !(((MethodDefEntry) method).getAccess().isSynthetic() && !parameters.includeSynthetic())) { ClassDefEntry def = this.entryIndex.getDefinition(containingClass); if (def != null && def.isRecord()) { if (this.isCanonicalConstructor(def, method) @@ -219,10 +218,10 @@ private StatsResult generate(Set includedTypes, ClassEntry classEntry, int index = ((MethodDefEntry) method).getAccess().isStatic() ? 0 : 1; for (ArgumentDescriptor argument : argumentDescs) { - if (!(argument.getAccess().isSynthetic() && !includeSynthetic) + if (!(argument.getAccess().isSynthetic() && !parameters.includeSynthetic()) // skip the implicit superclass parameter for non-static inner class constructors && !(method.isConstructor() && containingClass.isInnerClass() && index == 1 && argument.containsType() && argument.getTypeEntry().equals(containingClass.getOuterClass()))) { - this.update(StatType.PARAMETERS, mappableCounts, unmappedCounts, new LocalVariableEntry(method, index)); + this.update(StatType.PARAMETERS, mappableCounts, unmappedCounts, new LocalVariableEntry(method, index), parameters); } index += argument.getSize(); @@ -230,7 +229,7 @@ private StatsResult generate(Set includedTypes, ClassEntry classEntry, } } } else if (entry instanceof ClassEntry clazz && includedTypes.contains(StatType.CLASSES)) { - this.update(StatType.CLASSES, mappableCounts, unmappedCounts, clazz); + this.update(StatType.CLASSES, mappableCounts, unmappedCounts, clazz, parameters); } } @@ -291,10 +290,12 @@ public StatsResult getStats(ClassEntry entry) { return this.result.getStats().get(entry); } - private void update(StatType type, Map mappable, Map> unmapped, Entry entry) { + private void update(StatType type, Map mappable, Map> unmapped, Entry entry, GenerationParameters parameters) { if (this.project.isRenamable(entry)) { - if (this.project.isObfuscated(entry) && !this.project.isSynthetic(entry) - || this.fallbackNameProposerIdCache.contains(this.project.getRemapper().getMapping(entry).sourcePluginId())) { // fallback proposed mappings don't count + boolean obf = this.project.isObfuscated(entry); + + if ((obf && (!this.project.isSynthetic(entry) || !parameters.includeSynthetic())) + || (!parameters.countFallback() && this.fallbackNameProposerIdCache.contains(this.project.getRemapper().getMapping(entry).sourcePluginId()))) { // fallback proposed mappings don't count String parent = this.project.getRemapper().deobfuscate(entry.getTopLevelClass()).getName().replace('/', '.'); unmapped.computeIfAbsent(type, t -> new HashMap<>()); diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index d60b6f5a1..cdf703b12 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -42,6 +42,7 @@ "menu.file.stats.filter": "Filter", "menu.file.stats.top_level_package": "Top-Level Package:", "menu.file.stats.synthetic_parameters": "Include Synthetic Parameters", + "menu.file.stats.count_fallback": "Count Fallback-Proposed Mappings", "menu.file.stats.generate": "Generate Diagram", "menu.file.configure_keybinds": "Configure Keybinds...", "menu.file.configure_keybinds.title": "Configure Keybinds", @@ -75,6 +76,11 @@ "menu.view.scale": "Scale", "menu.view.scale.custom": "Custom...", "menu.view.scale.custom.title": "Custom Scale", + "menu.view.stat_icons": "Stat Icons", + "menu.view.stat_icons.include_synthetic": "Include synthetic parameters", + "menu.view.stat_icons.count_fallback": "Count fallback-proposed names", + "menu.view.stat_icons.included_types": "Included types", + "menu.view.stat_icons.enable_icons": "Enable icons", "menu.view.font": "Fonts...", "menu.view.change.title": "Changes", "menu.view.change.summary": "Changes will be applied after the next restart.", diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestInnerClassParameterStats.java b/enigma/src/test/java/org/quiltmc/enigma/TestInnerClassParameterStats.java index 7bb78f66a..f1f3fe6f9 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestInnerClassParameterStats.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestInnerClassParameterStats.java @@ -4,6 +4,7 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.class_provider.JarClassProvider; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.ProjectStatsResult; import org.quiltmc.enigma.api.stats.StatType; import org.quiltmc.enigma.api.stats.StatsGenerator; @@ -26,7 +27,7 @@ public class TestInnerClassParameterStats { @Test public void testInnerClassParameterStats() { EnigmaProject project = openProject(); - ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), EnumSet.of(StatType.PARAMETERS), null, false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), null, new GenerationParameters(EnumSet.of(StatType.PARAMETERS))); // 8/13 total parameters in our six classes are non-mappable, meaning that we should get 0/3 parameters mapped // these non-mappable parameters come from non-static inner classes taking their enclosing class as a parameter // they are currently manually excluded by a check in the stats generator diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexEnums.java b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexEnums.java index b73a1e98a..183b7ccba 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexEnums.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexEnums.java @@ -3,6 +3,7 @@ import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.ProjectStatsResult; import org.junit.jupiter.api.Test; import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; @@ -22,7 +23,7 @@ public class TestJarIndexEnums { @Test void checkEnumStats() { EnigmaProject project = openProject(); - ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), EnumSet.allOf(StatType.class), null, false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), null, new GenerationParameters(EnumSet.allOf(StatType.class))); assertThat(stats.getMapped(StatType.CLASSES), equalTo(0)); assertThat(stats.getMapped(StatType.FIELDS), equalTo(0)); diff --git a/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java index a82573902..65e3a1927 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java +++ b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java @@ -15,6 +15,7 @@ import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; import org.quiltmc.enigma.api.service.NameProposalService; import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.StatType; import org.quiltmc.enigma.api.stats.StatsGenerator; import org.quiltmc.enigma.api.translation.TranslateResult; @@ -29,10 +30,10 @@ import java.io.Reader; import java.io.StringReader; import java.nio.file.Path; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import static org.hamcrest.MatcherAssert.assertThat; @@ -80,13 +81,13 @@ public void testFallbackStats() throws IOException { assertMappingStartsWith(TestEntryFactory.newField(bClass, "a", "I"), TestEntryFactory.newField(bClass, "slay", "I")); assertMappingStartsWith(TestEntryFactory.newField(bClass, "a", "Ljava/lang/String;"), TestEntryFactory.newField(bClass, "slay", "Ljava/lang/String;")); - var proposerFieldStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.FIELDS), bClass, false); - var proposerMethodStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.METHODS), bClass, false); + var proposerFieldStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), bClass, new GenerationParameters(EnumSet.of(StatType.FIELDS))); + var proposerMethodStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), bClass, new GenerationParameters(EnumSet.of(StatType.METHODS))); project = Enigma.create().openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); - var controlFieldStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.FIELDS), bClass, false); - var controlMethodStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.METHODS), bClass, false); + var controlFieldStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), bClass, new GenerationParameters(EnumSet.of(StatType.FIELDS))); + var controlMethodStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), bClass, new GenerationParameters(EnumSet.of(StatType.METHODS))); // method stats should be identical since fallback proposals don't affect stats assertEquals(controlMethodStats.getMappable(), proposerMethodStats.getMappable()); diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordStats.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordStats.java index e8e9ec52f..036e95892 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordStats.java +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordStats.java @@ -9,6 +9,7 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.StatType; import org.quiltmc.enigma.api.stats.StatsGenerator; import org.quiltmc.enigma.api.stats.StatsResult; @@ -53,7 +54,7 @@ static void setupEnigma() throws IOException { @Test void testParameters() { - StatsResult stats = new StatsGenerator(project).generate(EnumSet.of(StatType.PARAMETERS), TestEntryFactory.newClass("c"), false); + StatsResult stats = new StatsGenerator(project).generate(TestEntryFactory.newClass("c"), new GenerationParameters(EnumSet.of(StatType.PARAMETERS))); // total params in the class are 10 // equals method is ignored @@ -66,14 +67,14 @@ void testParameters() { @Test void testMethods() { ClassEntry c = TestEntryFactory.newClass("c"); - StatsResult stats = new StatsGenerator(project).generate(EnumSet.of(StatType.METHODS), c, false); + StatsResult stats = new StatsGenerator(project).generate(c, new GenerationParameters(EnumSet.of(StatType.METHODS))); // 4 mappable methods: 1 for each field assertThat(stats.getMappable(StatType.METHODS), equalTo(4)); assertThat(stats.getMapped(StatType.METHODS), equalTo(0)); project.getRemapper().putMapping(TestUtil.newVC(), TestEntryFactory.newField(c, "a", "Ljava/lang/String;"), new EntryMapping("gaming")); - StatsResult stats2 = new StatsGenerator(project).generate(EnumSet.of(StatType.METHODS), c, false); + StatsResult stats2 = new StatsGenerator(project).generate(c, new GenerationParameters(EnumSet.of(StatType.METHODS))); // 1 method mapped to match field assertThat(stats2.getMappable(StatType.METHODS), equalTo(4)); diff --git a/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatGenerationInheritance.java b/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatGenerationInheritance.java index 9240184fb..7ce7599a2 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatGenerationInheritance.java +++ b/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatGenerationInheritance.java @@ -9,6 +9,7 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.class_provider.JarClassProvider; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.StatType; import org.quiltmc.enigma.api.stats.StatsGenerator; import org.quiltmc.enigma.api.stats.StatsResult; @@ -18,7 +19,7 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.Set; +import java.util.EnumSet; public class TestStatGenerationInheritance { private static final Path JAR = TestUtil.obfJar("interfaces"); @@ -40,17 +41,17 @@ void testInterfacePropagation() { ClassEntry interfaceEntry = TestEntryFactory.newClass("b"); ClassEntry inheritorEntry = TestEntryFactory.newClass("a"); - StatsResult interfaceResult = new StatsGenerator(project).generate(Set.of(StatType.METHODS), interfaceEntry, false); + StatsResult interfaceResult = new StatsGenerator(project).generate(interfaceEntry, new GenerationParameters(EnumSet.of(StatType.METHODS))); Assertions.assertEquals(2, interfaceResult.getMappable()); // the inheritor does not own the method; it won't count towards its stats - StatsResult inheritorResult = new StatsGenerator(project).generate(Set.of(StatType.METHODS), inheritorEntry, false); + StatsResult inheritorResult = new StatsGenerator(project).generate(inheritorEntry, new GenerationParameters(EnumSet.of(StatType.METHODS))); Assertions.assertEquals(0, inheritorResult.getMappable()); MethodEntry inheritedMethod = TestEntryFactory.newMethod(inheritorEntry, "a", "(D)D"); project.getRemapper().putMapping(TestUtil.newVC(), inheritedMethod, new EntryMapping("mapped")); - StatsResult interfaceResult2 = new StatsGenerator(project).generate(Set.of(StatType.METHODS), interfaceEntry, false); + StatsResult interfaceResult2 = new StatsGenerator(project).generate(interfaceEntry, new GenerationParameters(EnumSet.of(StatType.METHODS))); Assertions.assertEquals(1, interfaceResult2.getMapped()); } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatsGeneration.java b/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatsGeneration.java index a6f08abc1..24cfbdece 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatsGeneration.java +++ b/enigma/src/test/java/org/quiltmc/enigma/stats/TestStatsGeneration.java @@ -6,6 +6,7 @@ import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_provider.JarClassProvider; +import org.quiltmc.enigma.api.stats.GenerationParameters; import org.quiltmc.enigma.api.stats.ProjectStatsResult; import org.quiltmc.enigma.api.stats.StatType; import org.quiltmc.enigma.api.stats.StatsGenerator; @@ -19,6 +20,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Collection; +import java.util.EnumSet; import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; @@ -30,7 +32,7 @@ public class TestStatsGeneration { @Test void checkNoMappedEntriesByDefault() { EnigmaProject project = openProject(); - ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.values()), null, false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), null, new GenerationParameters(EnumSet.of(StatType.METHODS))); assertThat(stats.getMapped(), equalTo(0)); assertThat(stats.getPercentage(), equalTo(0d)); } @@ -90,7 +92,7 @@ private static EnigmaProject openProject() { } private static void checkFullyMapped(EnigmaProject project, StatType... types) { - ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(types), null, false); + ProjectStatsResult stats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), null, new GenerationParameters(Set.of(types))); assertThat(stats.getMapped(types), equalTo(stats.getMappable(types))); assertThat(stats.getPercentage(types), equalTo(100d)); }