mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
ResourceBundleEditor: added context menu for translation editors + action to propagate value across all of translations
This commit is contained in:
+47
-19
@@ -38,7 +38,6 @@ import com.intellij.lang.properties.psi.PropertiesFile;
|
||||
import com.intellij.lang.properties.psi.PropertiesResourceBundleUtil;
|
||||
import com.intellij.lang.properties.psi.impl.PropertyKeyImpl;
|
||||
import com.intellij.openapi.actionSystem.*;
|
||||
import com.intellij.openapi.actionSystem.ex.ActionUtil;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.command.CommandProcessor;
|
||||
import com.intellij.openapi.command.WriteCommandAction;
|
||||
@@ -50,7 +49,6 @@ import com.intellij.openapi.editor.*;
|
||||
import com.intellij.openapi.editor.actions.EditorActionUtil;
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager;
|
||||
import com.intellij.openapi.editor.colors.EditorColorsScheme;
|
||||
import com.intellij.openapi.editor.event.EditorMouseAdapter;
|
||||
import com.intellij.openapi.editor.event.EditorMouseEvent;
|
||||
import com.intellij.openapi.editor.event.EditorMouseEventArea;
|
||||
import com.intellij.openapi.editor.ex.EditorEx;
|
||||
@@ -85,7 +83,6 @@ import javax.swing.event.TreeSelectionListener;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.TreePath;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
@@ -100,7 +97,7 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
private final StructureViewComponent myStructureViewComponent;
|
||||
private final Map<PropertiesFile, Editor> myEditors;
|
||||
private final ResourceBundle myResourceBundle;
|
||||
private final ResourceBundlePropertiesInsertManager myPropertiesInsertDeleteManager;
|
||||
private final ResourceBundlePropertiesUpdateManager myPropertiesInsertDeleteManager;
|
||||
private final Map<PropertiesFile, JPanel> myTitledPanels;
|
||||
private final JComponent myNoPropertySelectedPanel = new NoPropertySelectedPanel().getComponent();
|
||||
private final Project myProject;
|
||||
@@ -134,7 +131,7 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
splitPanel.add(splitter, BorderLayout.CENTER);
|
||||
|
||||
myResourceBundle = resourceBundle;
|
||||
myPropertiesInsertDeleteManager = ResourceBundlePropertiesInsertManagerImpl.create(resourceBundle);
|
||||
myPropertiesInsertDeleteManager = ResourceBundlePropertiesUpdateManagerImpl.create(resourceBundle);
|
||||
|
||||
myPropertiesAnchorizer = new PropertiesAnchorizer(myResourceBundle.getProject());
|
||||
myStructureViewComponent = new ResourceBundleStructureViewComponent(myResourceBundle, this, myPropertiesAnchorizer);
|
||||
@@ -155,7 +152,7 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
if (e.getOldLeadSelectionPath() != null) {
|
||||
for (Map.Entry<PropertiesFile, Editor> entry : myEditors.entrySet()) {
|
||||
if (entry.getValue() == mySelectedEditor) {
|
||||
writeEditorPropertyValue(mySelectedEditor, entry.getKey(), selectedProperty.getName());
|
||||
writeEditorPropertyValue(selectedProperty.getName(), mySelectedEditor, entry.getKey());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -211,7 +208,7 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ResourceBundlePropertiesInsertManager getPropertiesInsertDeleteManager() {
|
||||
public ResourceBundlePropertiesUpdateManager getPropertiesInsertDeleteManager() {
|
||||
return myPropertiesInsertDeleteManager;
|
||||
}
|
||||
|
||||
@@ -341,7 +338,9 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
return value instanceof ResourceBundleEditorViewElement ? (ResourceBundleEditorViewElement) value : null;
|
||||
}
|
||||
|
||||
private void writeEditorPropertyValue(final Editor editor, final PropertiesFile propertiesFile, final @Nullable String propertyName) {
|
||||
private void writeEditorPropertyValue(final @Nullable String propertyName,
|
||||
final @NotNull Editor editor,
|
||||
final @NotNull PropertiesFile propertiesFile) {
|
||||
final String currentValue = editor.getDocument().getText();
|
||||
final String currentSelectedProperty = propertyName == null ? getSelectedPropertyName() : propertyName;
|
||||
if (currentSelectedProperty == null) {
|
||||
@@ -354,14 +353,8 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
WriteCommandAction.runWriteCommandAction(myProject, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final IProperty property = propertiesFile.findPropertyByKey(currentSelectedProperty);
|
||||
try {
|
||||
if (property == null) {
|
||||
myPropertiesInsertDeleteManager.insertTranslation(currentSelectedProperty, currentValue, propertiesFile);
|
||||
}
|
||||
else {
|
||||
property.setValue(currentValue);
|
||||
}
|
||||
myPropertiesInsertDeleteManager.insertOrUpdateTranslation(currentSelectedProperty, currentValue, propertiesFile);
|
||||
}
|
||||
catch (final IncorrectOperationException e) {
|
||||
LOG.error(e);
|
||||
@@ -418,7 +411,7 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
|
||||
@Override
|
||||
public void focusLost(final Editor eventEditor) {
|
||||
writeEditorPropertyValue(editor, propertiesFile, null);
|
||||
writeEditorPropertyValue(null, editor, propertiesFile);
|
||||
}
|
||||
});
|
||||
gc.gridx = 0;
|
||||
@@ -847,7 +840,7 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
if (mySelectedEditor != null) {
|
||||
for (final Map.Entry<PropertiesFile, Editor> entry : myEditors.entrySet()) {
|
||||
if (mySelectedEditor.equals(entry.getValue())) {
|
||||
writeEditorPropertyValue(mySelectedEditor, entry.getKey(), null);
|
||||
writeEditorPropertyValue(null, mySelectedEditor, entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -893,7 +886,7 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
return editor;
|
||||
}
|
||||
|
||||
private static void reinitSettings(final EditorEx editor) {
|
||||
private void reinitSettings(final EditorEx editor) {
|
||||
EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
|
||||
editor.setColorsScheme(scheme);
|
||||
EditorSettings settings = editor.getSettings();
|
||||
@@ -914,7 +907,42 @@ public class ResourceBundleEditor extends UserDataHolderBase implements FileEdit
|
||||
@Override
|
||||
public void invokePopup(EditorMouseEvent event) {
|
||||
if (!event.isConsumed() && event.getArea() == EditorMouseEventArea.EDITING_AREA) {
|
||||
ActionGroup group = (ActionGroup)CustomActionsSchema.getInstance().getCorrectedAction(IdeActions.GROUP_CUT_COPY_PASTE);
|
||||
DefaultActionGroup group = new DefaultActionGroup();
|
||||
group.copyFromGroup((DefaultActionGroup)CustomActionsSchema.getInstance().getCorrectedAction(IdeActions.GROUP_CUT_COPY_PASTE));
|
||||
group.addSeparator();
|
||||
group.add(new AnAction("Propagate Value Across of Resource Bundle") {
|
||||
@Override
|
||||
public void actionPerformed(AnActionEvent e) {
|
||||
final String valueToPropagate = editor.getDocument().getText();
|
||||
final String currentSelectedProperty = getSelectedPropertyName();
|
||||
if (currentSelectedProperty == null) {
|
||||
return;
|
||||
}
|
||||
ApplicationManager.getApplication().runWriteAction(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
WriteCommandAction.runWriteCommandAction(myProject, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
for (Map.Entry<PropertiesFile, Editor> entry : myEditors.entrySet()) {
|
||||
final Editor translationEditor = entry.getValue();
|
||||
if (translationEditor != editor) {
|
||||
final PropertiesFile propertiesFile = entry.getKey();
|
||||
myPropertiesInsertDeleteManager.insertOrUpdateTranslation(currentSelectedProperty, valueToPropagate, propertiesFile);
|
||||
translationEditor.getDocument().setText(valueToPropagate);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (final IncorrectOperationException e) {
|
||||
LOG.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
EditorPopupHandler handler = EditorActionUtil.createEditorPopupHandler(group);
|
||||
handler.invokePopup(event);
|
||||
event.consume();
|
||||
|
||||
+6
-5
@@ -16,26 +16,27 @@
|
||||
package com.intellij.lang.properties.editor;
|
||||
|
||||
import com.intellij.lang.properties.psi.PropertiesFile;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
|
||||
/**
|
||||
* @author Dmitry Batkovich
|
||||
*/
|
||||
public interface ResourceBundlePropertiesInsertManager {
|
||||
public interface ResourceBundlePropertiesUpdateManager {
|
||||
void insertNewProperty(String key, String value);
|
||||
|
||||
void insertTranslation(String key, String value, PropertiesFile propertiesFile);
|
||||
void insertOrUpdateTranslation(String key, String value, PropertiesFile propertiesFile) throws IncorrectOperationException;
|
||||
|
||||
void reload();
|
||||
|
||||
class Stub implements ResourceBundlePropertiesInsertManager {
|
||||
public static final ResourceBundlePropertiesInsertManager INSTANCE = new Stub();
|
||||
class Stub implements ResourceBundlePropertiesUpdateManager {
|
||||
public static final ResourceBundlePropertiesUpdateManager INSTANCE = new Stub();
|
||||
|
||||
@Override
|
||||
public void insertNewProperty(String key, String value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertTranslation(String key, String value, PropertiesFile propertiesFile) {
|
||||
public void insertOrUpdateTranslation(String key, String value, PropertiesFile propertiesFile) {
|
||||
}
|
||||
|
||||
@Override
|
||||
+16
-9
@@ -26,6 +26,7 @@ import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.psi.PsiComment;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiWhiteSpace;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import com.intellij.util.graph.CachingSemiGraph;
|
||||
import com.intellij.util.graph.DFSTBuilder;
|
||||
import com.intellij.util.graph.GraphGenerator;
|
||||
@@ -37,26 +38,26 @@ import java.util.*;
|
||||
/**
|
||||
* @author Dmitry Batkovich
|
||||
*/
|
||||
public class ResourceBundlePropertiesInsertManagerImpl implements ResourceBundlePropertiesInsertManager {
|
||||
private final static Logger LOG = Logger.getInstance(ResourceBundlePropertiesInsertManagerImpl.class);
|
||||
public class ResourceBundlePropertiesUpdateManagerImpl implements ResourceBundlePropertiesUpdateManager {
|
||||
private final static Logger LOG = Logger.getInstance(ResourceBundlePropertiesUpdateManagerImpl.class);
|
||||
|
||||
private final ResourceBundle myResourceBundle;
|
||||
private boolean myOrdered;
|
||||
private boolean myAlphaSorted;
|
||||
private List<String> myKeysOrder;
|
||||
|
||||
private ResourceBundlePropertiesInsertManagerImpl(ResourceBundle bundle) {
|
||||
private ResourceBundlePropertiesUpdateManagerImpl(ResourceBundle bundle) {
|
||||
myResourceBundle = bundle;
|
||||
reload();
|
||||
}
|
||||
|
||||
public static ResourceBundlePropertiesInsertManager create(ResourceBundle bundle) {
|
||||
public static ResourceBundlePropertiesUpdateManager create(ResourceBundle bundle) {
|
||||
for (PropertiesFile file : bundle.getPropertiesFiles()) {
|
||||
if (!(file instanceof PropertiesFileImpl)) {
|
||||
return ResourceBundlePropertiesInsertManager.Stub.INSTANCE;
|
||||
return ResourceBundlePropertiesUpdateManager.Stub.INSTANCE;
|
||||
}
|
||||
}
|
||||
return new ResourceBundlePropertiesInsertManagerImpl(bundle);
|
||||
return new ResourceBundlePropertiesUpdateManagerImpl(bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,15 +77,21 @@ public class ResourceBundlePropertiesInsertManagerImpl implements ResourceBundle
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertTranslation(String key, String value, final PropertiesFile propertiesFile) {
|
||||
public void insertOrUpdateTranslation(String key, String value, final PropertiesFile propertiesFile) throws IncorrectOperationException {
|
||||
final IProperty property = propertiesFile.findPropertyByKey(key);
|
||||
if (property != null) {
|
||||
property.setValue(value);
|
||||
}
|
||||
|
||||
if (myOrdered) {
|
||||
if (myAlphaSorted) {
|
||||
propertiesFile.addProperty(key, value);
|
||||
return;
|
||||
}
|
||||
final Pair<IProperty, Integer> propertyAndPosition = findExistedPrevSiblingProperty(key, propertiesFile);
|
||||
propertiesFile.addPropertyAfter(key, value, propertyAndPosition == null ? null :(Property)propertyAndPosition.getFirst());
|
||||
} else {
|
||||
propertiesFile.addPropertyAfter(key, value, propertyAndPosition == null ? null : (Property)propertyAndPosition.getFirst());
|
||||
}
|
||||
else {
|
||||
insertPropertyLast(key, value, propertiesFile);
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -26,11 +26,9 @@ import com.intellij.lang.properties.psi.Property;
|
||||
import com.intellij.openapi.actionSystem.*;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.ide.CopyPasteManager;
|
||||
import com.intellij.openapi.util.Key;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiNamedElement;
|
||||
import com.intellij.psi.impl.FakePsiElement;
|
||||
import com.intellij.psi.util.PsiUtilCore;
|
||||
import com.intellij.refactoring.safeDelete.SafeDeleteHandler;
|
||||
import com.intellij.ui.PopupHandler;
|
||||
@@ -149,10 +147,10 @@ public class ResourceBundleStructureViewComponent extends PropertiesGroupingStru
|
||||
}
|
||||
|
||||
private class PsiElementsDeleteProvider implements DeleteProvider {
|
||||
private final ResourceBundlePropertiesInsertManager myInsertDeleteManager;
|
||||
private final ResourceBundlePropertiesUpdateManager myInsertDeleteManager;
|
||||
private final PsiElement[] myElements;
|
||||
|
||||
private PsiElementsDeleteProvider(ResourceBundlePropertiesInsertManager insertDeleteManager, final PsiElement[] elements) {
|
||||
private PsiElementsDeleteProvider(ResourceBundlePropertiesUpdateManager insertDeleteManager, final PsiElement[] elements) {
|
||||
myInsertDeleteManager = insertDeleteManager;
|
||||
myElements = elements;
|
||||
}
|
||||
|
||||
+25
-24
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
package com.intellij.lang.properties;
|
||||
|
||||
import com.intellij.lang.properties.editor.ResourceBundlePropertiesInsertManager;
|
||||
import com.intellij.lang.properties.editor.ResourceBundlePropertiesInsertManagerImpl;
|
||||
import com.intellij.lang.properties.editor.ResourceBundlePropertiesUpdateManager;
|
||||
import com.intellij.lang.properties.editor.ResourceBundlePropertiesUpdateManagerImpl;
|
||||
import com.intellij.lang.properties.psi.PropertiesFile;
|
||||
import com.intellij.openapi.application.PluginPathManager;
|
||||
import com.intellij.psi.PsiFile;
|
||||
@@ -39,26 +39,27 @@ public class ResourceBundleEditorInsertManagerTest extends LightPlatformCodeInsi
|
||||
myFixture.configureByFile(getTestName(true) + "/p_fr.properties");
|
||||
final PsiFile file = myFixture.configureByFile(getTestName(true) + "/p_ru.properties");
|
||||
final PropertiesFile propertiesFile = PropertiesImplUtil.getPropertiesFile(file);
|
||||
final ResourceBundlePropertiesInsertManager manager = ResourceBundlePropertiesInsertManagerImpl.create(propertiesFile.getResourceBundle());
|
||||
final ResourceBundlePropertiesUpdateManager
|
||||
manager = ResourceBundlePropertiesUpdateManagerImpl.create(propertiesFile.getResourceBundle());
|
||||
manager.reload();
|
||||
assertTrue(((ResourceBundlePropertiesInsertManagerImpl)manager).isAlphaSorted());
|
||||
assertTrue(((ResourceBundlePropertiesUpdateManagerImpl)manager).isAlphaSorted());
|
||||
}
|
||||
|
||||
public void testAddToAlphaOrdered() {
|
||||
doTest(new TestAction() {
|
||||
@Override
|
||||
public void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesInsertManagerImpl manager) {
|
||||
public void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesUpdateManagerImpl manager) {
|
||||
assertTrue(manager.isAlphaSorted());
|
||||
manager.insertNewProperty("l", "v");
|
||||
manager.insertTranslation("d", "v", baseFile);
|
||||
manager.insertTranslation("r", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("d", "v", baseFile);
|
||||
manager.insertOrUpdateTranslation("r", "v", translationFile);
|
||||
manager.insertNewProperty("a", "v");
|
||||
manager.insertTranslation("a", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("a", "v", translationFile);
|
||||
manager.insertNewProperty("z", "v");
|
||||
manager.insertTranslation("z", "v", translationFile);
|
||||
manager.insertTranslation("l", "v", translationFile);
|
||||
manager.insertTranslation("e", "v", translationFile);
|
||||
manager.insertTranslation("t", "v", baseFile);
|
||||
manager.insertOrUpdateTranslation("z", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("l", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("e", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("t", "v", baseFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -66,13 +67,13 @@ public class ResourceBundleEditorInsertManagerTest extends LightPlatformCodeInsi
|
||||
public void testAddToOrdered() {
|
||||
doTest(new TestAction() {
|
||||
@Override
|
||||
public void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesInsertManagerImpl manager) {
|
||||
public void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesUpdateManagerImpl manager) {
|
||||
assertFalse(manager.isAlphaSorted());
|
||||
manager.insertNewProperty("bnm", "v");
|
||||
manager.insertNewProperty("uio", "v");
|
||||
manager.insertTranslation("uio", "v", translationFile);
|
||||
manager.insertTranslation("qwe", "v", translationFile);
|
||||
manager.insertTranslation("zxc", "v", baseFile);
|
||||
manager.insertOrUpdateTranslation("uio", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("qwe", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("zxc", "v", baseFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -80,13 +81,13 @@ public class ResourceBundleEditorInsertManagerTest extends LightPlatformCodeInsi
|
||||
public void testAddToUnordered() {
|
||||
doTest(new TestAction() {
|
||||
@Override
|
||||
public void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesInsertManagerImpl manager) {
|
||||
public void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesUpdateManagerImpl manager) {
|
||||
assertFalse(manager.isAlphaSorted());
|
||||
manager.insertTranslation("bnm", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("bnm", "v", translationFile);
|
||||
manager.insertNewProperty("ghj", "v");
|
||||
manager.insertTranslation("ghj", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("ghj", "v", translationFile);
|
||||
manager.insertNewProperty("uio", "v");
|
||||
manager.insertTranslation("uio", "v", translationFile);
|
||||
manager.insertOrUpdateTranslation("uio", "v", translationFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -97,15 +98,15 @@ public class ResourceBundleEditorInsertManagerTest extends LightPlatformCodeInsi
|
||||
final PropertiesFile basePropertiesFile = PropertiesImplUtil.getPropertiesFile(baseFile);
|
||||
assertNotNull(basePropertiesFile);
|
||||
final ResourceBundle bundle = basePropertiesFile.getResourceBundle();
|
||||
final ResourceBundlePropertiesInsertManager manager = ResourceBundlePropertiesInsertManagerImpl.create(bundle);
|
||||
assertInstanceOf(manager, ResourceBundlePropertiesInsertManagerImpl.class);
|
||||
final ResourceBundlePropertiesUpdateManager manager = ResourceBundlePropertiesUpdateManagerImpl.create(bundle);
|
||||
assertInstanceOf(manager, ResourceBundlePropertiesUpdateManagerImpl.class);
|
||||
testAction.doTestAction(basePropertiesFile, PropertiesImplUtil.getPropertiesFile(translationFile),
|
||||
(ResourceBundlePropertiesInsertManagerImpl)manager);
|
||||
(ResourceBundlePropertiesUpdateManagerImpl)manager);
|
||||
myFixture.checkResultByFile(getTestName(true) + "/p.properties", getTestName(true) + "/p-after.properties", true);
|
||||
myFixture.checkResultByFile(getTestName(true) + "/p_en.properties", getTestName(true) + "/p-after_en.properties",true);
|
||||
}
|
||||
|
||||
private interface TestAction {
|
||||
void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesInsertManagerImpl manager);
|
||||
void doTestAction(PropertiesFile baseFile, PropertiesFile translationFile, ResourceBundlePropertiesUpdateManagerImpl manager);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user