feat(gui): add reload and live reload actions (#1537)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -428,7 +428,7 @@ public class Utils {
|
||||
}
|
||||
|
||||
public static void checkThreadInterrupt() {
|
||||
if (Thread.interrupted()) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new JadxRuntimeException("Thread interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Long, IBackgroundTask> 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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -19,6 +19,7 @@ public class ProjectData {
|
||||
private List<TabViewState> openTabs = Collections.emptyList();
|
||||
private int activeTab = -1;
|
||||
private @Nullable Path cacheDir;
|
||||
private boolean enableLiveReload = false;
|
||||
|
||||
public List<Path> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<WatchKey, Path> keys = new HashMap<>();
|
||||
private final Map<Path, Set<Path>> files = new HashMap<>();
|
||||
private final AtomicBoolean cancel = new AtomicBoolean(false);
|
||||
private final BiConsumer<Path, WatchEvent.Kind<Path>> listener;
|
||||
|
||||
public FilesWatcher(List<Path> paths, BiConsumer<Path, WatchEvent.Kind<Path>> 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<Path>) event).context();
|
||||
Path path = dir.resolve(fileName);
|
||||
|
||||
Set<Path> files = this.files.get(dir);
|
||||
if (files == null || files.contains(path)) {
|
||||
listener.accept(path, (WatchEvent.Kind<Path>) 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<Path>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<Path> 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<Path> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 프로젝트로 저장
|
||||
|
||||
@@ -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 项目
|
||||
|
||||
@@ -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 專案
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#6E6E6E" fill-rule="evenodd" d="M12.5747152,11.8852806 C11.4741474,13.1817355 9.83247882,14.0044386 7.99865879,14.0044386 C5.03907292,14.0044386 2.57997332,11.8615894 2.08820756,9.0427473 L3.94774327,9.10768372 C4.43372186,10.8898575 6.06393114,12.2000519 8.00015362,12.2000519 C9.30149237,12.2000519 10.4645985,11.6082097 11.2349873,10.6790094 L9.05000019,8.71167959 L14.0431479,8.44999981 L14.3048222,13.4430431 L12.5747152,11.8852806 Z M3.42785637,4.11741586 C4.52839138,2.82452748 6.16775464,2.00443857 7.99865879,2.00443857 C10.918604,2.00443857 13.3513802,4.09026967 13.8882946,6.8532307 L12.0226389,6.78808057 C11.5024872,5.05935553 9.89838095,3.8000774 8.00015362,3.8000774 C6.69867367,3.8000774 5.53545628,4.39204806 4.76506921,5.32142241 L6.95482203,7.29304326 L1.96167436,7.55472304 L1.70000005,2.56167973 L3.42785637,4.11741586 Z" transform="rotate(3 8.002 8.004)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user