Merge pull request #19 from marhali/rc/1.3.0

Release Rc/1.3.0
This commit is contained in:
Marcel 2021-05-26 17:30:27 +02:00 committed by GitHub
commit b6d8251db3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 135 additions and 33 deletions

View File

@ -3,6 +3,18 @@
# easy-i18n Changelog # easy-i18n Changelog
## [Unreleased] ## [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] ## [1.2.0]
### Added ### Added
- Sorting for properties files - Sorting for properties files

View File

@ -3,7 +3,7 @@
pluginGroup = de.marhali.easyi18n pluginGroup = de.marhali.easyi18n
pluginName = easy-i18n pluginName = easy-i18n
pluginVersion = 1.2.0 pluginVersion = 1.3.0
pluginSinceBuild = 202 pluginSinceBuild = 202
pluginUntilBuild = 211.* pluginUntilBuild = 211.*
# Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl

View File

@ -54,11 +54,10 @@ public class TranslatorToolWindowFactory implements ToolWindowFactory {
// Initialize Window Manager // Initialize Window Manager
WindowManager.getInstance().initialize(toolWindow, treeView, tableView); WindowManager.getInstance().initialize(toolWindow, treeView, tableView);
// Initialize data store and load from disk // Synchronize ui with underlying data
DataStore store = DataStore.getInstance(project); DataStore store = DataStore.getInstance(project);
store.addSynchronizer(treeView); store.addSynchronizer(treeView);
store.addSynchronizer(tableView); store.addSynchronizer(tableView);
store.synchronize(null, null);
store.reloadFromDisk();
} }
} }

View File

@ -12,7 +12,8 @@ public interface DataSynchronizer {
/** /**
* Propagates data changes to implementation classes. * Propagates data changes to implementation classes.
* @param translations Updated translations model * @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);
} }

View File

@ -14,6 +14,10 @@ import java.util.List;
*/ */
public class Translations { public class Translations {
public static Translations empty() {
return new Translations(new ArrayList<>(), new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>()));
}
@NotNull @NotNull
private final List<String> locales; private final List<String> locales;
@ -34,7 +38,7 @@ public class Translations {
return locales; return locales;
} }
public LocalizedNode getNodes() { public @NotNull LocalizedNode getNodes() {
return nodes; return nodes;
} }

View File

@ -91,4 +91,42 @@ public class TreeModelTranslator extends DefaultTreeModel {
} }
} }
} }
public TreePath findTreePath(@NotNull String fullPath) {
List<String> 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);
}
} }

View File

@ -1,5 +1,7 @@
package de.marhali.easyi18n.service; package de.marhali.easyi18n.service;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.LocalizedNode; import de.marhali.easyi18n.model.LocalizedNode;
@ -17,15 +19,17 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
* Singleton service to manage localized messages. * Factory service to manage localized messages for multiple projects at once.
* @author marhali * @author marhali
*/ */
public class DataStore { public class DataStore {
private static DataStore INSTANCE; private static final Map<Project, DataStore> INSTANCES = new WeakHashMap<>();
private final Project project; private final Project project;
private final List<DataSynchronizer> synchronizer; private final List<DataSynchronizer> synchronizer;
@ -33,13 +37,24 @@ public class DataStore {
private Translations translations; private Translations translations;
private String searchQuery; private String searchQuery;
public static DataStore getInstance(Project project) { public static DataStore getInstance(@NotNull Project project) {
return INSTANCE == null ? INSTANCE = new DataStore(project) : INSTANCE; 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.project = project;
this.synchronizer = new ArrayList<>(); 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(); String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
if(localesPath == null || localesPath.isEmpty()) { if(localesPath == null || localesPath.isEmpty()) {
translations = new Translations(new ArrayList<>(), // Propagate empty state
new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>())); this.translations = Translations.empty();
synchronize(searchQuery, null);
} else { } else {
TranslatorIO io = IOUtil.determineFormat(localesPath); TranslatorIO io = IOUtil.determineFormat(localesPath);
io.read(project, localesPath, (translations) -> { io.read(project, localesPath, (loadedTranslations) -> {
if(translations != null) { // Read was successful this.translations = loadedTranslations == null ? Translations.empty() : loadedTranslations;
this.translations = translations; synchronize(searchQuery, null);
// 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<>()));
}
}); });
} }
} }
@ -100,7 +107,7 @@ public class DataStore {
*/ */
public void searchBeyKey(@Nullable String fullPath) { public void searchBeyKey(@Nullable String fullPath) {
// Use synchronizer to propagate search instance to all views // 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 if(!update.isDeletion()) { // Recreate with changed val / create
LocalizedNode node = translations.getOrCreateNode(update.getChange().getKey()); LocalizedNode node = translations.getOrCreateNode(update.getChange().getKey());
node.setValue(update.getChange().getTranslations()); node.setValue(update.getChange().getTranslations());
@ -141,7 +150,7 @@ public class DataStore {
// Persist changes and propagate them on success // Persist changes and propagate them on success
saveToDisk(success -> { saveToDisk(success -> {
if(success) { if(success) {
synchronizer.forEach(synchronizer -> synchronizer.synchronize(translations, searchQuery)); synchronize(searchQuery, scrollTo);
} }
}); });
} }
@ -149,7 +158,16 @@ public class DataStore {
/** /**
* @return Current translation state * @return Current translation state
*/ */
public Translations getTranslations() { public @NotNull Translations getTranslations() {
return translations; 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));
}
} }

View File

@ -24,7 +24,7 @@ public class I18nCompletionProvider extends CompletionProvider<CompletionParamet
@Override @Override
protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) { protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
Project project = parameters.getEditor().getProject(); Project project = parameters.getOriginalFile().getProject();
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale(); String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
String query = result.getPrefixMatcher().getPrefix(); String query = result.getPrefixMatcher().getPrefix();
@ -40,6 +40,10 @@ public class I18nCompletionProvider extends CompletionProvider<CompletionParamet
LocalizedNode node = sections.isEmpty() ? DataStore.getInstance(project).getTranslations().getNodes() LocalizedNode node = sections.isEmpty() ? DataStore.getInstance(project).getTranslations().getNodes()
: DataStore.getInstance(project).getTranslations().getNode(path); : DataStore.getInstance(project).getTranslations().getNode(path);
if(node == null) { // Unknown translation
return;
}
for(LocalizedNode children : node.getChildren()) { for(LocalizedNode children : node.getChildren()) {
if(lastSection == null || children.getKey().startsWith(lastSection)) { if(lastSection == null || children.getKey().startsWith(lastSection)) {
// Construct full key path / Fore nested objects add '.' to indicate deeper level // Construct full key path / Fore nested objects add '.' to indicate deeper level

View File

@ -20,6 +20,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@ -73,9 +74,26 @@ public class TableView implements DataSynchronizer {
} }
@Override @Override
public void synchronize(@NotNull Translations translations, @Nullable String searchQuery) { public void synchronize(@NotNull Translations translations,
@Nullable String searchQuery, @Nullable String scrollTo) {
table.setModel(new TableModelTranslator(translations, searchQuery, update -> table.setModel(new TableModelTranslator(translations, searchQuery, update ->
DataStore.getInstance(project).processUpdate(update))); 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() { public JPanel getRootPanel() {

View File

@ -20,6 +20,7 @@ import de.marhali.easyi18n.ui.dialog.EditDialog;
import de.marhali.easyi18n.ui.listener.DeleteKeyListener; import de.marhali.easyi18n.ui.listener.DeleteKeyListener;
import de.marhali.easyi18n.ui.listener.PopupClickListener; import de.marhali.easyi18n.ui.listener.PopupClickListener;
import de.marhali.easyi18n.ui.renderer.TreeRenderer; import de.marhali.easyi18n.ui.renderer.TreeRenderer;
import de.marhali.easyi18n.util.TranslationsUtil;
import de.marhali.easyi18n.util.TreeUtil; import de.marhali.easyi18n.util.TreeUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -75,12 +76,19 @@ public class TreeView implements DataSynchronizer {
} }
@Override @Override
public void synchronize(@NotNull Translations translations, @Nullable String searchQuery) { public void synchronize(@NotNull Translations translations,
tree.setModel(new TreeModelTranslator(project, translations, searchQuery)); @Nullable String searchQuery, @Nullable String scrollTo) {
TreeModelTranslator model = new TreeModelTranslator(project, translations, searchQuery);
tree.setModel(model);
if(searchQuery != null && !searchQuery.isEmpty()) { if(searchQuery != null && !searchQuery.isEmpty()) {
expandAll().run(); expandAll().run();
} }
if(scrollTo != null) {
tree.scrollPathToVisible(model.findTreePath(scrollTo));
}
} }
private void handlePopup(MouseEvent e) { private void handlePopup(MouseEvent e) {