From 65f7c802228d678c8d71b1a098d53a0faab51524 Mon Sep 17 00:00:00 2001 From: Skylot Date: Sat, 18 Jun 2022 20:20:11 +0100 Subject: [PATCH] feat(gui): add reload and live reload actions (#1537) --- .../java/jadx/core/dex/nodes/RootNode.java | 1 + .../src/main/java/jadx/core/utils/Utils.java | 2 +- .../jadx/gui/jobs/BackgroundExecutor.java | 44 +++--- .../java/jadx/gui/settings/JadxProject.java | 11 ++ .../jadx/gui/settings/data/ProjectData.java | 9 ++ .../src/main/java/jadx/gui/ui/MainWindow.java | 57 ++++++-- .../gui/ui/codearea/AbstractCodeArea.java | 3 +- .../gui/utils/fileswatcher/FilesWatcher.java | 125 ++++++++++++++++++ .../utils/fileswatcher/LiveReloadWorker.java | 92 +++++++++++++ .../java/jadx/gui/utils/ui/ActionHandler.java | 23 ++++ .../resources/i18n/Messages_de_DE.properties | 3 + .../resources/i18n/Messages_en_US.properties | 3 + .../resources/i18n/Messages_es_ES.properties | 3 + .../resources/i18n/Messages_ko_KR.properties | 3 + .../resources/i18n/Messages_zh_CN.properties | 3 + .../resources/i18n/Messages_zh_TW.properties | 3 + .../src/main/resources/icons/ui/refresh.svg | 4 + 17 files changed, 363 insertions(+), 26 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/FilesWatcher.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/LiveReloadWorker.java create mode 100644 jadx-gui/src/main/resources/icons/ui/refresh.svg diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index ed6eade5..6f77b06d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -269,6 +269,7 @@ public class RootNode { public void runPreDecompileStage() { boolean debugEnabled = LOG.isDebugEnabled(); for (IDexTreeVisitor pass : preDecompilePasses) { + Utils.checkThreadInterrupt(); long start = debugEnabled ? System.currentTimeMillis() : 0; try { pass.init(this); diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index 040373e0..648a7550 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -428,7 +428,7 @@ public class Utils { } public static void checkThreadInterrupt() { - if (Thread.interrupted()) { + if (Thread.currentThread().isInterrupted()) { throw new JadxRuntimeException("Thread interrupted"); } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index f4a872b6..2d07562e 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -3,6 +3,7 @@ package jadx.gui.jobs; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -11,6 +12,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; @@ -19,6 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.utils.NLS; @@ -33,16 +36,16 @@ import static jadx.gui.utils.UiUtils.calcProgress; public class BackgroundExecutor { private static final Logger LOG = LoggerFactory.getLogger(BackgroundExecutor.class); - private final MainWindow mainWindow; + private final JadxSettings settings; private final ProgressPanel progressPane; private ThreadPoolExecutor taskQueueExecutor; private final Map taskRunning = new ConcurrentHashMap<>(); private final AtomicLong idSupplier = new AtomicLong(0); - public BackgroundExecutor(MainWindow mainWindow) { - this.mainWindow = mainWindow; - this.progressPane = mainWindow.getProgressPane(); + public BackgroundExecutor(JadxSettings settings, ProgressPanel progressPane) { + this.settings = Objects.requireNonNull(settings); + this.progressPane = Objects.requireNonNull(progressPane); reset(); } @@ -68,9 +71,16 @@ public class BackgroundExecutor { public synchronized void cancelAll() { try { taskRunning.values().forEach(Cancelable::cancel); - taskQueueExecutor.shutdown(); - boolean complete = taskQueueExecutor.awaitTermination(30, TimeUnit.SECONDS); - LOG.debug("Background task executor terminated with status: {}", complete ? "complete" : "interrupted"); + taskQueueExecutor.shutdownNow(); + boolean complete = taskQueueExecutor.awaitTermination(3, TimeUnit.SECONDS); + if (complete) { + LOG.debug("Background task executor canceled successfully"); + } else { + String taskNames = taskRunning.values().stream() + .map(IBackgroundTask::getTitle) + .collect(Collectors.joining(", ")); + LOG.debug("Background task executor cancel failed. Running tasks: {}", taskNames); + } } catch (Exception e) { LOG.error("Error terminating task executor", e); } finally { @@ -131,8 +141,16 @@ public class BackgroundExecutor { try { runJobs(); } finally { - taskComplete(id); - task.onDone(this); + try { + task.onDone(this); + // treat UI task operations as part of the task to not mix with others + UiUtils.uiRunAndWait(() -> { + progressPane.setVisible(false); + task.onFinish(this); + }); + } finally { + taskComplete(id); + } } return status; } @@ -146,7 +164,7 @@ public class BackgroundExecutor { progressPane.changeVisibility(this, true); } status = TaskStatus.STARTED; - int threadsCount = mainWindow.getSettings().getThreadsCount(); + int threadsCount = settings.getThreadsCount(); executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount); for (Runnable job : jobs) { executor.execute(job); @@ -258,12 +276,6 @@ public class BackgroundExecutor { }; } - @Override - protected void done() { - progressPane.setVisible(false); - task.onFinish(this); - } - @Override public TaskStatus getStatus() { return status; diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java index a41cf6df..f45e6513 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java @@ -169,6 +169,17 @@ public class JadxProject { throw new JadxRuntimeException("Can't get working dir"); } + public boolean isEnableLiveReload() { + return data.isEnableLiveReload(); + } + + public void setEnableLiveReload(boolean newValue) { + if (newValue != data.isEnableLiveReload()) { + data.setEnableLiveReload(newValue); + changed(); + } + } + private void changed() { JadxSettings settings = mainWindow.getSettings(); if (settings != null && settings.isAutoSaveProject()) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java b/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java index b9f206f9..536d3b0b 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java @@ -19,6 +19,7 @@ public class ProjectData { private List openTabs = Collections.emptyList(); private int activeTab = -1; private @Nullable Path cacheDir; + private boolean enableLiveReload = false; public List getFiles() { return files; @@ -94,4 +95,12 @@ public class ProjectData { public void setCacheDir(Path cacheDir) { this.cacheDir = cacheDir; } + + public boolean isEnableLiveReload() { + return enableLiveReload; + } + + public void setEnableLiveReload(boolean enableLiveReload) { + this.enableLiveReload = enableLiveReload; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index c95c0276..24e496ec 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -16,6 +16,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; +import java.awt.event.InputEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; @@ -135,7 +136,9 @@ import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.SystemInfo; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.fileswatcher.LiveReloadWorker; import jadx.gui.utils.logs.LogCollector; +import jadx.gui.utils.ui.ActionHandler; import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE; import static javax.swing.KeyStroke.getKeyStroke; @@ -152,6 +155,7 @@ public class MainWindow extends JFrame { private static final ImageIcon ICON_OPEN = UiUtils.openSvgIcon("ui/openDisk"); private static final ImageIcon ICON_ADD_FILES = UiUtils.openSvgIcon("ui/addFile"); private static final ImageIcon ICON_SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall"); + private static final ImageIcon ICON_RELOAD = UiUtils.openSvgIcon("ui/refresh"); private static final ImageIcon ICON_EXPORT = UiUtils.openSvgIcon("ui/export"); private static final ImageIcon ICON_EXIT = UiUtils.openSvgIcon("ui/exit"); private static final ImageIcon ICON_SYNC = UiUtils.openSvgIcon("ui/pagination"); @@ -196,6 +200,9 @@ public class MainWindow extends JFrame { private JToggleButton deobfToggleBtn; private JCheckBoxMenuItem deobfMenuItem; + private JCheckBoxMenuItem liveReloadMenuItem; + private final LiveReloadWorker liveReloadWorker; + private transient Link updateLink; private transient ProgressPanel progressPane; private transient Theme editorTheme; @@ -208,18 +215,18 @@ public class MainWindow extends JFrame { this.cacheObject = new CacheObject(); this.project = new JadxProject(this); this.wrapper = new JadxWrapper(this); + this.liveReloadWorker = new LiveReloadWorker(this); resetCache(); FontUtils.registerBundledFonts(); initUI(); + this.backgroundExecutor = new BackgroundExecutor(settings, progressPane); initMenuAndToolbar(); registerMouseNavigationButtons(); UiUtils.setWindowIcons(this); loadSettings(); - update(); - - this.backgroundExecutor = new BackgroundExecutor(this); + update(); checkForUpdate(); } @@ -405,7 +412,7 @@ public class MainWindow extends JFrame { return loadedFile.resolveSibling(fileName); } - public void reopen() { + public synchronized void reopen() { saveAll(); closeAll(); loadFiles(EMPTY_RUNNABLE); @@ -439,6 +446,10 @@ public class MainWindow extends JFrame { UiUtils.errorMessage(this, NLS.str("message.memoryLow")); return; } + if (status != TaskStatus.COMPLETE) { + LOG.warn("Loading task incomplete, status: {}", status); + return; + } checkLoadedStatus(); onOpen(); exportMappingsMenu.setEnabled(true); @@ -485,6 +496,7 @@ public class MainWindow extends JFrame { deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); initTree(); update(); + updateLiveReload(project.isEnableLiveReload()); BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent()); backgroundExecutor.execute(NLS.str("progress.load"), @@ -492,6 +504,21 @@ public class MainWindow extends JFrame { status -> runInitialBackgroundJobs()); } + public void updateLiveReload(boolean state) { + if (liveReloadWorker.isStarted() == state) { + return; + } + project.setEnableLiveReload(state); + liveReloadMenuItem.setEnabled(false); + backgroundExecutor.execute( + (state ? "Starting" : "Stopping") + " live reload", + () -> liveReloadWorker.updateState(state), + s -> { + liveReloadMenuItem.setState(state); + liveReloadMenuItem.setEnabled(true); + }); + } + private void addTreeCustomNodes() { treeRoot.replaceCustomNode(ApkSignature.getApkSignature(wrapper)); treeRoot.replaceCustomNode(new SummaryNode(this)); @@ -829,6 +856,19 @@ public class MainWindow extends JFrame { }; saveProjectAsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project_as")); + ActionHandler reload = new ActionHandler(ev -> UiUtils.uiRun(this::reopen)); + reload.setNameAndDesc(NLS.str("file.reload")); + reload.setIcon(ICON_RELOAD); + reload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, 0)); + + ActionHandler liveReload = new ActionHandler(ev -> updateLiveReload(!project.isEnableLiveReload())); + liveReload.setName(NLS.str("file.live_reload")); + liveReload.setShortDescription(NLS.str("file.live_reload_desc")); + liveReload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, InputEvent.SHIFT_DOWN_MASK)); + + liveReloadMenuItem = new JCheckBoxMenuItem(liveReload); + liveReloadMenuItem.setState(project.isEnableLiveReload()); + Action exportMappingsAsTiny2 = new AbstractAction("Tiny v2 file") { @Override public void actionPerformed(ActionEvent e) { @@ -1045,6 +1085,9 @@ public class MainWindow extends JFrame { file.add(saveProjectAction); file.add(saveProjectAsAction); file.addSeparator(); + file.add(reload); + file.add(liveReloadMenuItem); + file.addSeparator(); file.add(exportMappingsMenu); file.addSeparator(); file.add(saveAllAction); @@ -1114,6 +1157,8 @@ public class MainWindow extends JFrame { toolbar.add(openAction); toolbar.add(addFilesAction); toolbar.addSeparator(); + toolbar.add(reload); + toolbar.addSeparator(); toolbar.add(saveAllAction); toolbar.add(exportAction); toolbar.addSeparator(); @@ -1452,10 +1497,6 @@ public class MainWindow extends JFrame { return backgroundExecutor; } - public ProgressPanel getProgressPane() { - return progressPane; - } - public JRoot getTreeRoot() { return treeRoot; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index fcf71bdc..787d4f12 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -11,6 +11,7 @@ import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; +import java.util.Objects; import javax.swing.AbstractAction; import javax.swing.Action; @@ -75,7 +76,7 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { public AbstractCodeArea(ContentPanel contentPanel, JNode node) { this.contentPanel = contentPanel; - this.node = node; + this.node = Objects.requireNonNull(node); setMarkOccurrences(false); setEditable(false); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/FilesWatcher.java b/jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/FilesWatcher.java new file mode 100644 index 00000000..403b4ad8 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/FilesWatcher.java @@ -0,0 +1,125 @@ +package jadx.gui.utils.fileswatcher; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.utils.Utils; + +import static java.nio.file.LinkOption.NOFOLLOW_LINKS; +import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; +import static java.nio.file.StandardWatchEventKinds.OVERFLOW; + +public class FilesWatcher { + private static final Logger LOG = LoggerFactory.getLogger(FilesWatcher.class); + + private final WatchService watcher = FileSystems.getDefault().newWatchService(); + private final Map keys = new HashMap<>(); + private final Map> files = new HashMap<>(); + private final AtomicBoolean cancel = new AtomicBoolean(false); + private final BiConsumer> listener; + + public FilesWatcher(List paths, BiConsumer> listener) throws IOException { + this.listener = listener; + for (Path path : paths) { + if (Files.isDirectory(path, NOFOLLOW_LINKS)) { + registerDirs(path); + } else { + Path parentDir = path.toAbsolutePath().getParent(); + register(parentDir); + files.merge(parentDir, Collections.singleton(path), Utils::mergeSets); + } + } + } + + public void cancel() { + cancel.set(true); + } + + @SuppressWarnings("unchecked") + public void watch() { + cancel.set(false); + LOG.debug("File watcher started for {} dirs", keys.size()); + while (!cancel.get()) { + WatchKey key; + try { + key = watcher.take(); + } catch (InterruptedException e) { + LOG.debug("File watcher interrupted"); + return; + } + Path dir = keys.get(key); + if (dir == null) { + LOG.warn("Unknown directory key: {}", key); + continue; + } + for (WatchEvent event : key.pollEvents()) { + if (cancel.get() || Thread.interrupted()) { + return; + } + WatchEvent.Kind kind = event.kind(); + if (kind == OVERFLOW) { + continue; + } + Path fileName = ((WatchEvent) event).context(); + Path path = dir.resolve(fileName); + + Set files = this.files.get(dir); + if (files == null || files.contains(path)) { + listener.accept(path, (WatchEvent.Kind) kind); + } + if (kind == ENTRY_CREATE) { + try { + if (Files.isDirectory(path, NOFOLLOW_LINKS)) { + registerDirs(path); + } + } catch (Exception e) { + LOG.warn("Failed to update directory watch: {}", path, e); + } + } + } + boolean valid = key.reset(); + if (!valid) { + keys.remove(key); + if (keys.isEmpty()) { + LOG.debug("File watcher stopped: all watch keys removed"); + return; + } + } + } + } + + private void registerDirs(Path start) throws IOException { + Files.walkFileTree(start, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + register(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + private void register(Path dir) throws IOException { + WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + keys.put(key, dir); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/LiveReloadWorker.java b/jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/LiveReloadWorker.java new file mode 100644 index 00000000..10c321e2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/LiveReloadWorker.java @@ -0,0 +1,92 @@ +package jadx.gui.utils.fileswatcher; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.reactivex.processors.PublishProcessor; + +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.UiUtils; + +public class LiveReloadWorker { + private static final Logger LOG = LoggerFactory.getLogger(LiveReloadWorker.class); + + private final MainWindow mainWindow; + private final PublishProcessor processor; + private volatile boolean started = false; + private ExecutorService executor; + private FilesWatcher watcher; + + @SuppressWarnings("ResultOfMethodCallIgnored") + public LiveReloadWorker(MainWindow mainWindow) { + this.mainWindow = mainWindow; + this.processor = PublishProcessor.create(); + this.processor + .debounce(1, TimeUnit.SECONDS) + .subscribe(path -> { + LOG.debug("Reload triggered"); + UiUtils.uiRun(mainWindow::reopen); + }); + } + + public boolean isStarted() { + return started; + } + + public synchronized void updateState(boolean enabled) { + if (this.started == enabled) { + return; + } + if (enabled) { + LOG.debug("Starting live reload worker"); + start(); + } else { + LOG.debug("Stopping live reload worker"); + stop(); + } + } + + private void onUpdate(Path path, WatchEvent.Kind pathKind) { + LOG.debug("Path updated: {}", path); + processor.onNext(path); + } + + private synchronized void start() { + try { + watcher = new FilesWatcher(mainWindow.getProject().getFilePaths(), this::onUpdate); + executor = Executors.newSingleThreadExecutor(); + started = true; + executor.submit(watcher::watch); + } catch (Exception e) { + LOG.warn("Failed to start live reload worker", e); + resetState(); + } + } + + private synchronized void stop() { + try { + watcher.cancel(); + executor.shutdownNow(); + boolean canceled = executor.awaitTermination(5, TimeUnit.SECONDS); + if (!canceled) { + LOG.warn("Failed to cancel live reload worker"); + } + } catch (Exception e) { + LOG.warn("Failed to stop live reload worker", e); + } finally { + resetState(); + } + } + + private void resetState() { + started = false; + executor = null; + watcher = null; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java index 46ceacf9..390a3f8d 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java @@ -4,6 +4,8 @@ import java.awt.event.ActionEvent; import java.util.function.Consumer; import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.KeyStroke; public class ActionHandler extends AbstractAction { @@ -13,6 +15,27 @@ public class ActionHandler extends AbstractAction { this.consumer = consumer; } + public void setName(String name) { + putValue(NAME, name); + } + + public void setNameAndDesc(String name) { + setName(name); + setShortDescription(name); + } + + public void setShortDescription(String desc) { + putValue(SHORT_DESCRIPTION, desc); + } + + public void setIcon(ImageIcon icon) { + putValue(SMALL_ICON, icon); + } + + public void setKeyBinding(KeyStroke keyStroke) { + putValue(ACCELERATOR_KEY, keyStroke); + } + @Override public void actionPerformed(ActionEvent e) { consumer.accept(e); diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 30db55eb..f94313bb 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -26,6 +26,9 @@ file.open_title=Datei öffnen file.new_project=Neues Projekt file.save_project=Projekt speichern file.save_project_as=Projekt speichern als… +#file.reload=Reload files +#file.live_reload=Live reload +#file.live_reload_desc=Auto reload files on changes #file.export_mappings_as= file.save_all=Alles speichern file.export_gradle=Als Gradle-Projekt speichern diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 7e9715ff..d11a20a0 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -26,6 +26,9 @@ file.open_title=Open file file.new_project=New project file.save_project=Save project file.save_project_as=Save project as... +file.reload=Reload files +file.live_reload=Live reload +file.live_reload_desc=Auto reload files on changes file.export_mappings_as=Export mappings as... file.save_all=Save all file.export_gradle=Save as gradle project diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 7c380f8d..f4ea2daf 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -26,6 +26,9 @@ file.open_title=Abrir archivo #file.new_project= #file.save_project= #file.save_project_as= +#file.reload=Reload files +#file.live_reload=Live reload +#file.live_reload_desc=Auto reload files on changes #file.export_mappings_as= file.save_all=Guardar todo file.export_gradle=Guardar como proyecto Gradle diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 2e697883..83e0ef73 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -26,6 +26,9 @@ file.open_title=파일 열기 file.new_project=새 프로젝트 file.save_project=프로젝트 저장 file.save_project_as=다른 이름으로 프로젝트 저장... +#file.reload=Reload files +#file.live_reload=Live reload +#file.live_reload_desc=Auto reload files on changes #file.export_mappings_as= file.save_all=모두 저장 file.export_gradle=Gradle 프로젝트로 저장 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 44c3af12..85daf547 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -26,6 +26,9 @@ file.open_title=打开文件 file.new_project=新建项目 file.save_project=保存项目 file.save_project_as=另存项目为... +#file.reload=Reload files +#file.live_reload=Live reload +#file.live_reload_desc=Auto reload files on changes #file.export_mappings_as= file.save_all=全部保存 file.export_gradle=另存为 Gradle 项目 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 5f641c61..5cb8b067 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -26,6 +26,9 @@ file.open_title=開啟檔案 file.new_project=新建專案 file.save_project=儲存專案 file.save_project_as=另存專案... +#file.reload=Reload files +#file.live_reload=Live reload +#file.live_reload_desc=Auto reload files on changes #file.export_mappings_as= file.save_all=全部儲存 file.export_gradle=另存為 gradle 專案 diff --git a/jadx-gui/src/main/resources/icons/ui/refresh.svg b/jadx-gui/src/main/resources/icons/ui/refresh.svg new file mode 100644 index 00000000..f33d3dde --- /dev/null +++ b/jadx-gui/src/main/resources/icons/ui/refresh.svg @@ -0,0 +1,4 @@ + + + + -- GitLab