diff --git a/CHANGELOG.md b/CHANGELOG.md index d60e5e2..85609c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ # easy-i18n Changelog ## [Unreleased] +### Added +- Scroll to created / edited translation inside Tree-/Table-View +- Support for working with multiple projects at once + +### Changed +- Updated dependencies +- Load translations even if ui tool window is not opened + +### Fixed +- NullPointerException's on translation annotation / completion inside editor +- Always synchronize ui with loaded state by reloadFromDisk function + ## [1.2.0] ### Added - Sorting for properties files diff --git a/gradle.properties b/gradle.properties index 3ad8dd4..795629b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ pluginGroup = de.marhali.easyi18n pluginName = easy-i18n -pluginVersion = 1.2.0 +pluginVersion = 1.3.0 pluginSinceBuild = 202 pluginUntilBuild = 211.* # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl diff --git a/src/main/java/de/marhali/easyi18n/TranslatorToolWindowFactory.java b/src/main/java/de/marhali/easyi18n/TranslatorToolWindowFactory.java index 38efd01..281419f 100644 --- a/src/main/java/de/marhali/easyi18n/TranslatorToolWindowFactory.java +++ b/src/main/java/de/marhali/easyi18n/TranslatorToolWindowFactory.java @@ -54,11 +54,10 @@ public class TranslatorToolWindowFactory implements ToolWindowFactory { // Initialize Window Manager WindowManager.getInstance().initialize(toolWindow, treeView, tableView); - // Initialize data store and load from disk + // Synchronize ui with underlying data DataStore store = DataStore.getInstance(project); store.addSynchronizer(treeView); store.addSynchronizer(tableView); - - store.reloadFromDisk(); + store.synchronize(null, null); } } \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/model/DataSynchronizer.java b/src/main/java/de/marhali/easyi18n/model/DataSynchronizer.java index fc25fe4..7cc3e71 100644 --- a/src/main/java/de/marhali/easyi18n/model/DataSynchronizer.java +++ b/src/main/java/de/marhali/easyi18n/model/DataSynchronizer.java @@ -12,7 +12,8 @@ public interface DataSynchronizer { /** * Propagates data changes to implementation classes. * @param translations Updated translations model - * @param searchQuery Can be used to filter visible data. Like a search function for the full key path. + * @param searchQuery Can be used to filter visible data. Like a search function for the full key path + * @param scrollToKey Focus specific translation. Can be null to disable this function */ - void synchronize(@NotNull Translations translations, @Nullable String searchQuery); + void synchronize(@NotNull Translations translations, @Nullable String searchQuery, @Nullable String scrollToKey); } \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/model/Translations.java b/src/main/java/de/marhali/easyi18n/model/Translations.java index 0d84dd5..44bd03f 100644 --- a/src/main/java/de/marhali/easyi18n/model/Translations.java +++ b/src/main/java/de/marhali/easyi18n/model/Translations.java @@ -14,6 +14,10 @@ import java.util.List; */ public class Translations { + public static Translations empty() { + return new Translations(new ArrayList<>(), new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>())); + } + @NotNull private final List locales; @@ -34,7 +38,7 @@ public class Translations { return locales; } - public LocalizedNode getNodes() { + public @NotNull LocalizedNode getNodes() { return nodes; } diff --git a/src/main/java/de/marhali/easyi18n/model/tree/TreeModelTranslator.java b/src/main/java/de/marhali/easyi18n/model/tree/TreeModelTranslator.java index dc87a52..a479deb 100644 --- a/src/main/java/de/marhali/easyi18n/model/tree/TreeModelTranslator.java +++ b/src/main/java/de/marhali/easyi18n/model/tree/TreeModelTranslator.java @@ -91,4 +91,42 @@ public class TreeModelTranslator extends DefaultTreeModel { } } } + + public TreePath findTreePath(@NotNull String fullPath) { + List sections = TranslationsUtil.getSections(fullPath); + Object[] nodes = new Object[sections.size() + 1]; + + int pos = 0; + TreeNode currentNode = (TreeNode) this.getRoot(); + nodes[pos] = currentNode; + + for(String section : sections) { + pos++; + currentNode = findNode(currentNode, section); + nodes[pos] = currentNode; + } + + return new TreePath(nodes); + } + + public @Nullable DefaultMutableTreeNode findNode(@NotNull TreeNode parent, @NotNull String key) { + for(int i = 0; i < parent.getChildCount(); i++) { + TreeNode child = parent.getChildAt(i); + + if(child instanceof DefaultMutableTreeNode) { + DefaultMutableTreeNode mutableChild = (DefaultMutableTreeNode) child; + String childKey = mutableChild.getUserObject().toString(); + + if(mutableChild.getUserObject() instanceof PresentationData) { + childKey = ((PresentationData) mutableChild.getUserObject()).getPresentableText(); + } + + if(childKey != null && childKey.equals(key)) { + return mutableChild; + } + } + } + + throw new NullPointerException("Cannot find node by key: " + key); + } } diff --git a/src/main/java/de/marhali/easyi18n/service/DataStore.java b/src/main/java/de/marhali/easyi18n/service/DataStore.java index 319b146..4047bba 100644 --- a/src/main/java/de/marhali/easyi18n/service/DataStore.java +++ b/src/main/java/de/marhali/easyi18n/service/DataStore.java @@ -1,5 +1,7 @@ package de.marhali.easyi18n.service; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.project.Project; import de.marhali.easyi18n.model.LocalizedNode; @@ -17,15 +19,17 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; import java.util.function.Consumer; /** - * Singleton service to manage localized messages. + * Factory service to manage localized messages for multiple projects at once. * @author marhali */ public class DataStore { - private static DataStore INSTANCE; + private static final Map INSTANCES = new WeakHashMap<>(); private final Project project; private final List synchronizer; @@ -33,13 +37,24 @@ public class DataStore { private Translations translations; private String searchQuery; - public static DataStore getInstance(Project project) { - return INSTANCE == null ? INSTANCE = new DataStore(project) : INSTANCE; + public static DataStore getInstance(@NotNull Project project) { + DataStore store = INSTANCES.get(project); + + if(store == null) { + store = new DataStore(project); + INSTANCES.put(project, store); + } + + return store; } - private DataStore(Project project) { + private DataStore(@NotNull Project project) { this.project = project; this.synchronizer = new ArrayList<>(); + this.translations = Translations.empty(); + + // Load data after first initialization + ApplicationManager.getApplication().invokeLater(this::reloadFromDisk, ModalityState.NON_MODAL); } /** @@ -57,24 +72,16 @@ public class DataStore { String localesPath = SettingsService.getInstance(project).getState().getLocalesPath(); if(localesPath == null || localesPath.isEmpty()) { - translations = new Translations(new ArrayList<>(), - new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>())); + // Propagate empty state + this.translations = Translations.empty(); + synchronize(searchQuery, null); } else { TranslatorIO io = IOUtil.determineFormat(localesPath); - io.read(project, localesPath, (translations) -> { - if(translations != null) { // Read was successful - this.translations = translations; - - // Propagate changes - synchronizer.forEach(synchronizer -> synchronizer.synchronize(translations, searchQuery)); - - } else { - // If state cannot be loaded from disk, show empty instance - this.translations = new Translations(new ArrayList<>(), - new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>())); - } + io.read(project, localesPath, (loadedTranslations) -> { + this.translations = loadedTranslations == null ? Translations.empty() : loadedTranslations; + synchronize(searchQuery, null); }); } } @@ -100,7 +107,7 @@ public class DataStore { */ public void searchBeyKey(@Nullable String fullPath) { // Use synchronizer to propagate search instance to all views - synchronizer.forEach(synchronizer -> synchronizer.synchronize(translations, this.searchQuery = fullPath)); + synchronize(this.searchQuery = fullPath, null); } /** @@ -133,6 +140,8 @@ public class DataStore { } } + String scrollTo = update.isDeletion() ? null : update.getChange().getKey(); + if(!update.isDeletion()) { // Recreate with changed val / create LocalizedNode node = translations.getOrCreateNode(update.getChange().getKey()); node.setValue(update.getChange().getTranslations()); @@ -141,7 +150,7 @@ public class DataStore { // Persist changes and propagate them on success saveToDisk(success -> { if(success) { - synchronizer.forEach(synchronizer -> synchronizer.synchronize(translations, searchQuery)); + synchronize(searchQuery, scrollTo); } }); } @@ -149,7 +158,16 @@ public class DataStore { /** * @return Current translation state */ - public Translations getTranslations() { + public @NotNull Translations getTranslations() { return translations; } + + /** + * Synchronizes current translation's state to all connected subscribers. + * @param searchQuery Optional search by full key filter (ui view) + * @param scrollTo Optional scroll to full key (ui view) + */ + public void synchronize(@Nullable String searchQuery, @Nullable String scrollTo) { + synchronizer.forEach(subscriber -> subscriber.synchronize(this.translations, searchQuery, scrollTo)); + } } \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/ui/editor/I18nCompletionProvider.java b/src/main/java/de/marhali/easyi18n/ui/editor/I18nCompletionProvider.java index 75e44a0..bc77b00 100644 --- a/src/main/java/de/marhali/easyi18n/ui/editor/I18nCompletionProvider.java +++ b/src/main/java/de/marhali/easyi18n/ui/editor/I18nCompletionProvider.java @@ -24,7 +24,7 @@ public class I18nCompletionProvider extends CompletionProvider DataStore.getInstance(project).processUpdate(update))); + + if(scrollTo != null) { + int row = -1; + + for (int i = 0; i < table.getRowCount(); i++) { + if (String.valueOf(table.getValueAt(i, 0)).equals(scrollTo)) { + row = i; + } + } + + if (row > -1) { // Matched @scrollTo + table.scrollRectToVisible( + new Rectangle(0, (row * table.getRowHeight()) + table.getHeight(), 0, 0)); + } + } } public JPanel getRootPanel() { diff --git a/src/main/java/de/marhali/easyi18n/ui/tabs/TreeView.java b/src/main/java/de/marhali/easyi18n/ui/tabs/TreeView.java index 8014ca5..a4b77cd 100644 --- a/src/main/java/de/marhali/easyi18n/ui/tabs/TreeView.java +++ b/src/main/java/de/marhali/easyi18n/ui/tabs/TreeView.java @@ -20,6 +20,7 @@ import de.marhali.easyi18n.ui.dialog.EditDialog; import de.marhali.easyi18n.ui.listener.DeleteKeyListener; import de.marhali.easyi18n.ui.listener.PopupClickListener; import de.marhali.easyi18n.ui.renderer.TreeRenderer; +import de.marhali.easyi18n.util.TranslationsUtil; import de.marhali.easyi18n.util.TreeUtil; import org.jetbrains.annotations.NotNull; @@ -75,12 +76,19 @@ public class TreeView implements DataSynchronizer { } @Override - public void synchronize(@NotNull Translations translations, @Nullable String searchQuery) { - tree.setModel(new TreeModelTranslator(project, translations, searchQuery)); + public void synchronize(@NotNull Translations translations, + @Nullable String searchQuery, @Nullable String scrollTo) { + + TreeModelTranslator model = new TreeModelTranslator(project, translations, searchQuery); + tree.setModel(model); if(searchQuery != null && !searchQuery.isEmpty()) { expandAll().run(); } + + if(scrollTo != null) { + tree.scrollPathToVisible(model.findTreePath(scrollTo)); + } } private void handlePopup(MouseEvent e) {