blob: aca119d2c84bb5e02ab1bc58692907b5ceace101 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.editors.strings;
import com.android.builder.model.ProductFlavor;
import com.android.builder.model.Variant;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.xml.AndroidManifestParser;
import com.android.ide.common.xml.ManifestData;
import com.android.io.FileWrapper;
import com.android.tools.idea.configurations.LocaleMenuAction;
import com.android.tools.idea.editors.strings.table.*;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.model.ManifestInfo;
import com.android.tools.idea.rendering.LocalResourceRepository;
import com.android.tools.idea.rendering.Locale;
import com.google.common.collect.ImmutableSet;
import com.intellij.icons.AllIcons;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.*;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBLoadingPanel;
import com.intellij.ui.table.JBTable;
import icons.AndroidIcons;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.AndroidRootUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public class StringResourceViewPanel implements HyperlinkListener {
private static final boolean HIDE_TRANSLATION_ORDER_LINK = Boolean.getBoolean("hide.order.translations");
private final AndroidFacet myFacet;
private JPanel myContainer;
private JBLoadingPanel myLoadingPanel;
private JBTable myTable;
private JTextField myKey;
private TextFieldWithBrowseButton myDefaultValueWithBrowseBtn;
private final JTextField myDefaultValue;
private TextFieldWithBrowseButton myTranslationWithBrowseBtn;
private final JTextField myTranslation;
private JPanel myToolbarPanel;
private LocalResourceRepository myResourceRepository;
private long myModificationCount;
private StringResourceData myData;
private final StringResourceTableModel myTableModel;
public StringResourceViewPanel(AndroidFacet facet, Disposable parentDisposable) {
myFacet = facet;
myLoadingPanel = new JBLoadingPanel(new BorderLayout(), parentDisposable, 200);
myLoadingPanel.add(myContainer);
ActionToolbar toolbar = createToolbar();
myToolbarPanel.add(toolbar.getComponent(), BorderLayout.CENTER);
if (!HIDE_TRANSLATION_ORDER_LINK) {
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel("Order a translation...");
myToolbarPanel.add(hyperlinkLabel, BorderLayout.EAST);
hyperlinkLabel.addHyperlinkListener(this);
myToolbarPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));
}
myDefaultValueWithBrowseBtn.setButtonIcon(AllIcons.Actions.ShowViewer);
myTranslationWithBrowseBtn.setButtonIcon(AllIcons.Actions.ShowViewer);
ActionListener showMultilineActionListener = new ShowMultilineActionListener();
myDefaultValueWithBrowseBtn.addActionListener(showMultilineActionListener);
myTranslationWithBrowseBtn.addActionListener(showMultilineActionListener);
myDefaultValue = myDefaultValueWithBrowseBtn.getTextField();
myTranslation = myTranslationWithBrowseBtn.getTextField();
initEditPanel();
initTable();
myTableModel = new StringResourceTableModel(null);
myTable.setModel(myTableModel);
myLoadingPanel.setLoadingText("Loading string resource data");
myLoadingPanel.startLoading();
new ParseTask("Loading string resource data").queue();
}
public void reloadData() {
myLoadingPanel.setLoadingText("Updating string resource data");
myLoadingPanel.startLoading();
new ParseTask("Updating string resource data").queue();
}
private ActionToolbar createToolbar() {
DefaultActionGroup group = new DefaultActionGroup();
final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, true);
final AnAction addKeyAction = new AnAction("Add Key", "", AllIcons.ToolbarDecorator.Add) {
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myData != null);
}
@Override
public void actionPerformed(AnActionEvent e) {
NewStringKeyDialog dialog = new NewStringKeyDialog(myFacet, ImmutableSet.copyOf(myData.getKeys()));
if (dialog.showAndGet()) {
StringsWriteUtils.createItem(myFacet, dialog.getResFolder(), null, dialog.getKey(), dialog.getDefaultValue(), true);
reloadData();
}
}
};
group.add(addKeyAction);
final AnAction addLocaleAction = new AnAction("Add Locale", "", AndroidIcons.Globe) {
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myData != null);
}
@Override
public void actionPerformed(AnActionEvent e) {
List<Locale> currentLocales = myData.getLocales();
List<Locale> missingLocales = LocaleMenuAction.getAllLocales();
missingLocales.removeAll(currentLocales);
Collections.sort(missingLocales, Locale.LANGUAGE_NAME_COMPARATOR);
final JBList list = new JBList(missingLocales);
list.setFixedCellHeight(20);
list.setCellRenderer(new ColoredListCellRenderer<Locale>() {
@Override
protected void customizeCellRenderer(JList list, Locale value, int index, boolean selected, boolean hasFocus) {
append(LocaleMenuAction.getLocaleLabel(value, false));
setIcon(value.getFlagImage());
}
});
new ListSpeedSearch(list) {
@Override
protected String getElementText(Object element) {
if (element instanceof Locale) {
return LocaleMenuAction.getLocaleLabel((Locale)element, false);
}
return super.getElementText(element);
}
};
JBPopupFactory.getInstance().createListPopupBuilder(list).setItemChoosenCallback(new Runnable() {
@Override
public void run() {
// pick some value to add to this locale
Map<String, ResourceItem> defaultValues = myData.getDefaultValues();
String key = "app_name";
ResourceItem defaultValue = defaultValues.get(key);
if (defaultValue == null) {
Map.Entry<String, ResourceItem> firstEntry = defaultValues.entrySet().iterator().next();
key = firstEntry.getKey();
defaultValue = firstEntry.getValue();
}
Locale l = (Locale)list.getSelectedValue();
StringsWriteUtils.createItem(myFacet, myFacet.getPrimaryResourceDir(), l, key,
StringResourceData.resourceToString(defaultValue), true);
reloadData();
}
}).createPopup().showUnderneathOf(toolbar.getComponent());
}
};
group.addAction(addLocaleAction);
return toolbar;
}
public boolean dataIsCurrent() {
return myResourceRepository != null && myModificationCount >= myResourceRepository.getModificationCount();
}
private void initEditPanel() {
KeyListener keyListener = new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
JTextComponent component = (JTextComponent)e.getComponent();
if (component.isEditable()) {
onTextFieldUpdate(component);
}
}
};
myKey.addKeyListener(keyListener);
myDefaultValue.addKeyListener(keyListener);
myTranslation.addKeyListener(keyListener);
}
private void onTextFieldUpdate(JTextComponent component) {
StringResourceTableModel model = (StringResourceTableModel)myTable.getModel();
if (myTable.getSelectedColumnCount() != 1 || myTable.getSelectedRowCount() != 1) {
return;
}
int row = myTable.getSelectedRow();
int column;
if (component == myKey) {
column = ConstantColumn.KEY.ordinal();
}
else if (component == myDefaultValue) {
column = ConstantColumn.DEFAULT_VALUE.ordinal();
}
else {
assert component == myTranslation;
column = myTable.getSelectedColumn();
}
String value = component.getText();
model.setValueAt(value, row, column);
}
private void initTable() {
myTable.setCellSelectionEnabled(true);
myTable.getTableHeader().setReorderingAllowed(false);
MouseAdapter headerListener = new HeaderCellSelectionListener(myTable);
myTable.getTableHeader().addMouseListener(headerListener);
myTable.getTableHeader().addMouseMotionListener(headerListener);
CellSelectionListener selectionListener = new CellSelectionListener();
myTable.getSelectionModel().addListSelectionListener(selectionListener);
myTable.getColumnModel().getSelectionModel().addListSelectionListener(selectionListener);
myTable.setDefaultEditor(String.class, new StringsCellEditor());
myTable.getParent().addComponentListener(new ResizeListener(myTable));
}
@NotNull
public JPanel getComponent() {
return myLoadingPanel;
}
@NotNull
public JBTable getPreferredFocusedComponent() {
return myTable;
}
private void createUIComponents() {
myTable = new JBTable();
}
public JTable getTable() {
return myTable;
}
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
StringBuilder sb = new StringBuilder("https://translate.google.com/manager/android_studio/");
// Application Version
sb.append("?asVer=");
ApplicationInfo ideInfo = ApplicationInfo.getInstance();
sb.append(ideInfo.getMajorVersion()).append('.')
.append(ideInfo.getMinorVersion()).append('.')
.append(ideInfo.getMicroVersion()).append('.')
.append(ideInfo.getPatchVersion());
// Package name
String pkg = ManifestInfo.get(myFacet.getModule(), false).getPackage();
if (pkg != null) {
sb.append("&pkgName=");
sb.append(pkg.replace('.', '_'));
}
// Application ID
AndroidModuleInfo moduleInfo = AndroidModuleInfo.get(myFacet);
String appId = moduleInfo.getPackage();
if (appId != null) {
sb.append("&appId=");
sb.append(appId.replace('.', '_'));
}
// Version code
String versionCode = null;
if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) {
Variant variant = myFacet.getAndroidModel().getSelectedVariant();
ProductFlavor flavor = variant.getMergedFlavor();
if (flavor.getVersionCode() != null) {
versionCode = flavor.getVersionCode().toString();
}
}
if (versionCode == null) {
VirtualFile manifestFile = AndroidRootUtil.getPrimaryManifestFile(myFacet);
if (manifestFile != null) {
File file = VfsUtilCore.virtualToIoFile(manifestFile);
try {
ManifestData manifest = AndroidManifestParser.parse(new FileWrapper(file));
if (manifest.getVersionCode() != null) {
versionCode = manifest.getVersionCode().toString();
}
}
catch (Exception ignore) {
}
}
}
if (versionCode != null) {
sb.append("&apkVer=");
sb.append(versionCode);
}
// If we support additional IDE languages, we can send the language used in the IDE here
//sb.append("&lang=en");
BrowserUtil.browse(sb.toString());
}
private class ParseTask extends Task.Backgroundable {
private AtomicReference<LocalResourceRepository> myResourceRepositoryRef = new AtomicReference<LocalResourceRepository>(null);
private AtomicReference<StringResourceData> myResourceDataRef = new AtomicReference<StringResourceData>(null);
public ParseTask(String description) {
super(myFacet.getModule().getProject(), description, false);
}
@Override
public void run(@NotNull ProgressIndicator indicator) {
indicator.setIndeterminate(true);
LocalResourceRepository moduleResources = myFacet.getModuleResources(true);
myResourceRepositoryRef.set(moduleResources);
myResourceDataRef.set(StringResourceParser.parse(myFacet, moduleResources));
}
@Override
public void onSuccess() {
myLoadingPanel.stopLoading();
myData = myResourceDataRef.get();
myResourceRepository = myResourceRepositoryRef.get();
myModificationCount = myResourceRepository.getModificationCount();
myTableModel.setData(myData);
myTableModel.fireTableStructureChanged();
ColumnUtil.setColumns(myTable);
}
@Override
public void onCancel() {
myLoadingPanel.stopLoading();
}
}
private class CellSelectionListener implements ListSelectionListener {
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting()) {
return;
}
if (myTable.getSelectedColumnCount() != 1 || myTable.getSelectedRowCount() != 1) {
setTextAndEditable(myKey, "", false);
setTextAndEditable(myDefaultValue, "", false);
setTextAndEditable(myTranslation, "", false);
myDefaultValueWithBrowseBtn.getButton().setEnabled(false);
myTranslationWithBrowseBtn.getButton().setEnabled(false);
return;
}
StringResourceTableModel model = (StringResourceTableModel) myTable.getModel();
int row = myTable.getSelectedRow();
int column = myTable.getSelectedColumn();
Locale locale = model.localeOfColumn(column);
String key = model.keyOfRow(row);
setTextAndEditable(myKey, key, false); // TODO: keys are not editable, we want them to be refactor operations
String defaultValue = (String)model.getValueAt(row, ConstantColumn.DEFAULT_VALUE.ordinal());
boolean defaultValueEditable = !StringUtil.containsChar(defaultValue, '\n'); // don't allow editing multiline chars in a text field
setTextAndEditable(myDefaultValue, defaultValue, defaultValueEditable);
myDefaultValueWithBrowseBtn.getButton().setEnabled(true);
boolean translationEditable = false;
String translation = "";
if (locale != null) {
translation = (String)model.getValueAt(row, column);
translationEditable = !StringUtil.containsChar(translation, '\n'); // don't allow editing multiline chars in a text field
}
setTextAndEditable(myTranslation, translation, translationEditable);
myTranslationWithBrowseBtn.getButton().setEnabled(locale != null);
}
private void setTextAndEditable(@NotNull JTextComponent component, @NotNull String text, boolean editable) {
component.setText(text);
component.setCaretPosition(0);
component.setEditable(editable);
// If a text component is not editable when it gains focus and becomes editable while still focused,
// the caret does not appear, so we need to set the caret visibility manually
component.getCaret().setVisible(editable && component.hasFocus());
component.setFont(FontUtil.getFontAbleToDisplay(text, component.getFont()));
}
}
private class ShowMultilineActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (myTable.getSelectedRowCount() != 1 || myTable.getSelectedColumnCount() != 1) {
return;
}
int row = myTable.getSelectedRow();
int column = myTable.getSelectedColumn();
StringResourceTableModel model = (StringResourceTableModel)myTable.getModel();
String key = model.keyOfRow(row);
String value = (String)model.getValueAt(row, ConstantColumn.DEFAULT_VALUE.ordinal());
Locale locale = model.localeOfColumn(column);
String translation = locale == null ? null : (String)model.getValueAt(row, column);
MultilineStringEditorDialog d = new MultilineStringEditorDialog(myFacet, key, value, locale, translation);
if (d.showAndGet()) {
if (!StringUtil.equals(value, d.getDefaultValue())) {
model.setValueAt(d.getDefaultValue(), row, ConstantColumn.DEFAULT_VALUE.ordinal());
}
if (locale != null && !StringUtil.equals(translation, d.getTranslation())) {
model.setValueAt(d.getTranslation(), row, column);
}
}
}
}
}