blob: 0301b80fe2bad8ec3518080476913db2e3ed7631 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.ide.eclipse.adt.internal.wizards.newxmlfile;
import static com.android.SdkConstants.FD_RES;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.RES_QUALIFIER_SEP;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.ValueXmlHelper;
import com.android.ide.common.resources.LocaleManager;
import com.android.ide.common.resources.ResourceItem;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtUtils;
import com.android.ide.eclipse.adt.internal.editors.layout.configuration.FlagManager;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager;
import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
import com.android.resources.ResourceType;
import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.jface.viewers.EditingSupport;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
/**
* Dialog which adds a new translation to the project
*/
public class AddTranslationDialog extends Dialog implements ControlListener, SelectionListener,
TraverseListener {
private static final int KEY_COLUMN = 0;
private static final int DEFAULT_TRANSLATION_COLUMN = 1;
private static final int NEW_TRANSLATION_COLUMN = 2;
private final FolderConfiguration mConfiguration = new FolderConfiguration();
private final IProject mProject;
private String mTarget;
private boolean mIgnore;
private Map<String, String> mTranslations;
private Set<String> mExistingLanguages;
private String mSelectedLanguage;
private String mSelectedRegion;
private Table mTable;
private Combo mLanguageCombo;
private Combo mRegionCombo;
private ImageControl mFlag;
private Label mFile;
private Button mOkButton;
private Composite mErrorPanel;
private Label mErrorLabel;
private MyTableViewer mTableViewer;
/**
* Creates the dialog.
* @param parentShell the parent shell
* @param project the project to add translations into
*/
public AddTranslationDialog(Shell parentShell, IProject project) {
super(parentShell);
setShellStyle(SWT.CLOSE | SWT.RESIZE | SWT.TITLE);
mProject = project;
}
@Override
protected Control createDialogArea(Composite parent) {
Composite container = (Composite) super.createDialogArea(parent);
GridLayout gl_container = new GridLayout(6, false);
gl_container.horizontalSpacing = 0;
container.setLayout(gl_container);
Label languageLabel = new Label(container, SWT.NONE);
languageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
languageLabel.setText("Language:");
mLanguageCombo = new Combo(container, SWT.READ_ONLY);
GridData gd_mLanguageCombo = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1);
gd_mLanguageCombo.widthHint = 150;
mLanguageCombo.setLayoutData(gd_mLanguageCombo);
Label regionLabel = new Label(container, SWT.NONE);
GridData gd_regionLabel = new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1);
gd_regionLabel.horizontalIndent = 10;
regionLabel.setLayoutData(gd_regionLabel);
regionLabel.setText("Region:");
mRegionCombo = new Combo(container, SWT.READ_ONLY);
GridData gd_mRegionCombo = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1);
gd_mRegionCombo.widthHint = 150;
mRegionCombo.setLayoutData(gd_mRegionCombo);
mRegionCombo.setEnabled(false);
mFlag = new ImageControl(container, SWT.NONE, null);
mFlag.setDisposeImage(false);
GridData gd_mFlag = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1);
gd_mFlag.exclude = true;
gd_mFlag.widthHint = 32;
gd_mFlag.horizontalIndent = 3;
mFlag.setLayoutData(gd_mFlag);
mFile = new Label(container, SWT.NONE);
mFile.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
mTableViewer = new MyTableViewer(container, SWT.BORDER | SWT.FULL_SELECTION);
mTable = mTableViewer.getTable();
mTable.setEnabled(false);
mTable.setLinesVisible(true);
mTable.setHeaderVisible(true);
mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 6, 2));
mTable.addControlListener(this);
mTable.addTraverseListener(this);
// If you have difficulty opening up this form in WindowBuilder and it complains about
// the next line, change the type of the mTableViewer field and the above
// constructor call from MyTableViewer to TableViewer
TableViewerColumn keyViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE);
TableColumn keyColumn = keyViewerColumn.getColumn();
keyColumn.setWidth(100);
keyColumn.setText("Key");
TableViewerColumn defaultViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE);
TableColumn defaultColumn = defaultViewerColumn.getColumn();
defaultColumn.setWidth(200);
defaultColumn.setText("Default");
TableViewerColumn translationViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE);
TableColumn translationColumn = translationViewerColumn.getColumn();
translationColumn.setWidth(200);
translationColumn.setText("New Translation");
mErrorPanel = new Composite(container, SWT.NONE);
GridData gd_mErrorLabel = new GridData(SWT.FILL, SWT.CENTER, false, false, 6, 1);
gd_mErrorLabel.exclude = true;
mErrorPanel.setLayoutData(gd_mErrorLabel);
translationViewerColumn.setEditingSupport(new TranslationEditingSupport(mTableViewer));
fillLanguages();
fillRegions();
fillStrings();
updateColumnWidths();
validatePage();
mLanguageCombo.addSelectionListener(this);
mRegionCombo.addSelectionListener(this);
return container;
}
/** Populates the table with keys and default strings */
private void fillStrings() {
ResourceManager manager = ResourceManager.getInstance();
ProjectResources resources = manager.getProjectResources(mProject);
mExistingLanguages = resources.getLanguages();
Collection<ResourceItem> items = resources.getResourceItemsOfType(ResourceType.STRING);
ResourceItem[] array = items.toArray(new ResourceItem[items.size()]);
Arrays.sort(array);
// TODO: Read in the actual XML files providing the default keys here
// (they can be obtained via ResourceItem.getSourceFileList())
// such that we can read all the attributes associated with each
// item, and if it defines translatable=false, or the filename is
// donottranslate.xml, we can ignore it, and in other cases just
// duplicate all the attributes (such as "formatted=true", or other
// local conventions such as "product=tablet", or "msgid="123123123",
// etc.)
mTranslations = Maps.newHashMapWithExpectedSize(items.size());
IBaseLabelProvider labelProvider = new CellLabelProvider() {
@Override
public void update(ViewerCell cell) {
Object element = cell.getElement();
int index = cell.getColumnIndex();
ResourceItem item = (ResourceItem) element;
switch (index) {
case KEY_COLUMN: {
// Key
cell.setText(item.getName());
return;
}
case DEFAULT_TRANSLATION_COLUMN: {
// Default translation
ResourceValue value = item.getResourceValue(ResourceType.STRING,
mConfiguration, false);
if (value != null) {
cell.setText(value.getValue());
return;
}
break;
}
case NEW_TRANSLATION_COLUMN: {
// New translation
String translation = mTranslations.get(item.getName());
if (translation != null) {
cell.setText(translation);
return;
}
break;
}
default:
assert false : index;
}
cell.setText("");
}
};
mTableViewer.setLabelProvider(labelProvider);
mTableViewer.setContentProvider(new ArrayContentProvider());
mTableViewer.setInput(array);
}
/** Populate the languages dropdown */
private void fillLanguages() {
List<String> languageCodes = LocaleManager.getLanguageCodes();
List<String> labels = new ArrayList<String>();
for (String code : languageCodes) {
labels.add(code + ": " + LocaleManager.getLanguageName(code)); //$NON-NLS-1$
}
Collections.sort(labels);
labels.add(0, "(Select)");
mLanguageCombo.setItems(labels.toArray(new String[labels.size()]));
mLanguageCombo.select(0);
}
/** Populate the regions dropdown */
private void fillRegions() {
// TODO: When you switch languages, offer some "default" usable options. For example,
// when you choose English, offer the countries that use English, and so on. Unfortunately
// we don't have good data about this, we'd just need to hardcode a few common cases.
List<String> regionCodes = LocaleManager.getRegionCodes();
List<String> labels = new ArrayList<String>();
for (String code : regionCodes) {
labels.add(code + ": " + LocaleManager.getRegionName(code)); //$NON-NLS-1$
}
Collections.sort(labels);
labels.add(0, "Any");
mRegionCombo.setItems(labels.toArray(new String[labels.size()]));
mRegionCombo.select(0);
}
/** React to resizing by distributing the space evenly between the last two columns */
private void updateColumnWidths() {
Rectangle r = mTable.getClientArea();
int availableWidth = r.width;
// Distribute all available space to the last two columns
int columnCount = mTable.getColumnCount();
for (int i = 0; i < columnCount; i++) {
TableColumn column = mTable.getColumn(i);
availableWidth -= column.getWidth();
}
if (availableWidth != 0) {
TableColumn column = mTable.getColumn(DEFAULT_TRANSLATION_COLUMN);
column.setWidth(column.getWidth() + availableWidth / 2);
column = mTable.getColumn(NEW_TRANSLATION_COLUMN);
column.setWidth(column.getWidth() + availableWidth / 2 + availableWidth % 2);
}
}
@Override
protected void createButtonsForButtonBar(Composite parent) {
mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL,
// Don't make the OK button default as in most dialogs, since when you press
// Return thinking you might edit a value it dismisses the dialog instead
false);
createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
mOkButton.setEnabled(false);
validatePage();
}
/**
* Return the initial size of the dialog.
*/
@Override
protected Point getInitialSize() {
return new Point(800, 600);
}
private void updateTarget() {
if (mSelectedLanguage == null) {
mTarget = null;
mFile.setText("");
} else {
String folder = FD_RES + '/' + FD_RES_VALUES + RES_QUALIFIER_SEP + mSelectedLanguage;
if (mSelectedRegion != null) {
folder = folder + RES_QUALIFIER_SEP + 'r' + mSelectedRegion;
}
mTarget = folder + "/strings.xml"; //$NON-NLS-1$
mFile.setText(String.format("Creating %1$s", mTarget));
}
}
private void updateFlag() {
if (mSelectedLanguage == null) {
// Nothing selected
((GridData) mFlag.getLayoutData()).exclude = true;
} else {
FlagManager manager = FlagManager.get();
Image flag = manager.getFlag(mSelectedLanguage, mSelectedRegion);
if (flag != null) {
((GridData) mFlag.getLayoutData()).exclude = false;
mFlag.setImage(flag);
}
}
mFlag.getParent().layout(true);
mFlag.getParent().redraw();
}
/** Actually create the new translation file and write it to disk */
private void createTranslation() {
List<String> keys = new ArrayList<String>(mTranslations.keySet());
Collections.sort(keys);
StringBuilder sb = new StringBuilder(keys.size() * 120);
sb.append("<resources>\n\n"); //$NON-NLS-1$
for (String key : keys) {
String value = mTranslations.get(key);
if (value == null || value.trim().isEmpty()) {
continue;
}
sb.append(" <string name=\""); //$NON-NLS-1$
sb.append(key);
sb.append("\">"); //$NON-NLS-1$
sb.append(ValueXmlHelper.escapeResourceString(value));
sb.append("</string>\n"); //$NON-NLS-1$
}
sb.append("\n</resources>"); //$NON-NLS-1$
IFile file = mProject.getFile(mTarget);
try {
IContainer parent = file.getParent();
AdtUtils.ensureExists(parent);
InputStream source = new ByteArrayInputStream(sb.toString().getBytes(Charsets.UTF_8));
file.create(source, true, new NullProgressMonitor());
AdtPlugin.openFile(file, null, true /*showEditorTab*/);
// Ensure that the project resources updates itself to notice the new language.
// In theory, this shouldn't be necessary.
ResourceManager manager = ResourceManager.getInstance();
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IFolder folder = root.getFolder(parent.getFullPath());
manager.getResourceFolder(folder);
RenderPreviewManager.bumpRevision();
} catch (CoreException e) {
AdtPlugin.log(e, null);
}
}
private void validatePage() {
if (mOkButton == null) { // Early initialization
return;
}
String message = null;
if (mSelectedLanguage == null) {
message = "Select a language";
} else if (mExistingLanguages.contains(mSelectedLanguage)) {
if (mSelectedRegion == null) {
message = String.format("%1$s is already translated in this project",
LocaleManager.getLanguageName(mSelectedLanguage));
} else {
ResourceManager manager = ResourceManager.getInstance();
ProjectResources resources = manager.getProjectResources(mProject);
SortedSet<String> regions = resources.getRegions(mSelectedLanguage);
if (regions.contains(mSelectedRegion)) {
message = String.format("%1$s (%2$s) is already translated in this project",
LocaleManager.getLanguageName(mSelectedLanguage),
LocaleManager.getRegionName(mSelectedRegion));
}
}
} else {
// Require all strings to be translated? No, some of these may not
// be translatable (e.g. translatable=false, defined in donottranslate.xml, etc.)
//int missing = mTable.getItemCount() - mTranslations.values().size();
//if (missing > 0) {
// message = String.format("Missing %1$d translations", missing);
//}
}
boolean valid = message == null;
mTable.setEnabled(message == null);
mOkButton.setEnabled(valid);
showError(message);
}
private void showError(String error) {
GridData data = (GridData) mErrorPanel.getLayoutData();
boolean show = error != null;
if (show == data.exclude) {
if (show) {
if (mErrorLabel == null) {
mErrorPanel.setLayout(new GridLayout(2, false));
IWorkbench workbench = PlatformUI.getWorkbench();
ISharedImages sharedImages = workbench.getSharedImages();
String iconName = ISharedImages.IMG_OBJS_ERROR_TSK;
Image image = sharedImages.getImage(iconName);
@SuppressWarnings("unused")
ImageControl icon = new ImageControl(mErrorPanel, SWT.NONE, image);
mErrorLabel = new Label(mErrorPanel, SWT.NONE);
mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
1, 1));
}
mErrorLabel.setText(error);
}
data.exclude = !show;
mErrorPanel.getParent().layout(true);
}
}
@Override
protected void okPressed() {
mTableViewer.applyEditorValue();
super.okPressed();
createTranslation();
}
// ---- Implements ControlListener ----
@Override
public void controlMoved(ControlEvent e) {
}
@Override
public void controlResized(ControlEvent e) {
if (mIgnore) {
return;
}
updateColumnWidths();
}
// ---- Implements SelectionListener ----
@Override
public void widgetSelected(SelectionEvent e) {
if (mIgnore) {
return;
}
Object source = e.getSource();
if (source == mLanguageCombo) {
try {
mIgnore = true;
mRegionCombo.select(0);
mSelectedRegion = null;
} finally {
mIgnore = false;
}
int languageIndex = mLanguageCombo.getSelectionIndex();
if (languageIndex == 0) {
mSelectedLanguage = null;
mRegionCombo.setEnabled(false);
} else {
// This depends on the label format
mSelectedLanguage = mLanguageCombo.getItem(languageIndex).substring(0, 2);
mRegionCombo.setEnabled(true);
}
updateTarget();
updateFlag();
} else if (source == mRegionCombo) {
int regionIndex = mRegionCombo.getSelectionIndex();
if (regionIndex == 0) {
mSelectedRegion = null;
} else {
mSelectedRegion = mRegionCombo.getItem(regionIndex).substring(0, 2);
}
updateTarget();
updateFlag();
}
try {
mIgnore = true;
validatePage();
} finally {
mIgnore = false;
}
}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
}
// ---- TraverseListener ----
@Override
public void keyTraversed(TraverseEvent e) {
// If you press Return and we're not cell editing, start editing the current row
if (e.detail == SWT.TRAVERSE_RETURN && !mTableViewer.isCellEditorActive()) {
int index = mTable.getSelectionIndex();
if (index != -1) {
Object next = mTable.getItem(index).getData();
mTableViewer.editElement(next, NEW_TRANSLATION_COLUMN);
}
}
}
/** Editing support for the translation column */
private class TranslationEditingSupport extends EditingSupport {
/**
* When true, setValue is being called as part of a default action
* (e.g. Return), not due to focus loss
*/
private boolean mDefaultAction;
private TranslationEditingSupport(ColumnViewer viewer) {
super(viewer);
}
@Override
protected void setValue(Object element, Object value) {
ResourceItem item = (ResourceItem) element;
mTranslations.put(item.getName(), value.toString());
mTableViewer.update(element, null);
validatePage();
// If the user is pressing Return to finish editing a value (which is
// not the only way this method can get called - for example, if you click
// outside the cell while editing, the focus loss will also result in
// this method getting called), then mDefaultAction is true, and we automatically
// start editing the next row.
if (mDefaultAction) {
mTable.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (!mTable.isDisposed() && !mTableViewer.isCellEditorActive()) {
int index = mTable.getSelectionIndex();
if (index != -1 && index < mTable.getItemCount() - 1) {
Object next = mTable.getItem(index + 1).getData();
mTableViewer.editElement(next, NEW_TRANSLATION_COLUMN);
}
}
}
});
}
}
@Override
protected Object getValue(Object element) {
ResourceItem item = (ResourceItem) element;
String value = mTranslations.get(item.getName());
if (value == null) {
return "";
}
return value;
}
@Override
protected CellEditor getCellEditor(Object element) {
return new TextCellEditor(mTable) {
@Override
protected void handleDefaultSelection(SelectionEvent event) {
try {
mDefaultAction = true;
super.handleDefaultSelection(event);
} finally {
mDefaultAction = false;
}
}
};
}
@Override
protected boolean canEdit(Object element) {
return true;
}
}
private class MyTableViewer extends TableViewer {
public MyTableViewer(Composite parent, int style) {
super(parent, style);
}
// Make this public so we can call it to ensure values are applied before the dialog
// is dismissed in {@link #okPressed}
@Override
public void applyEditorValue() {
super.applyEditorValue();
}
}
}