replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / ui-designer / src / com / intellij / uiDesigner / propertyInspector / editors / string / StringEditorDialog.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.uiDesigner.propertyInspector.editors.string;
17
18 import com.intellij.CommonBundle;
19 import com.intellij.ide.util.TreeClassChooserFactory;
20 import com.intellij.ide.util.TreeFileChooser;
21 import com.intellij.lang.properties.IProperty;
22 import com.intellij.lang.properties.PropertiesReferenceManager;
23 import com.intellij.lang.properties.PropertiesUtilBase;
24 import com.intellij.lang.properties.psi.PropertiesFile;
25 import com.intellij.lang.properties.psi.Property;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.command.CommandProcessor;
28 import com.intellij.openapi.command.undo.UndoUtil;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.fileTypes.StdFileTypes;
31 import com.intellij.openapi.module.Module;
32 import com.intellij.openapi.progress.ProgressManager;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.ui.DialogWrapper;
35 import com.intellij.openapi.ui.InputValidator;
36 import com.intellij.openapi.ui.Messages;
37 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
38 import com.intellij.openapi.vfs.ReadonlyStatusHandler;
39 import com.intellij.openapi.vfs.VirtualFile;
40 import com.intellij.psi.PsiDocumentManager;
41 import com.intellij.psi.PsiFile;
42 import com.intellij.psi.PsiMethod;
43 import com.intellij.psi.PsiReference;
44 import com.intellij.psi.search.GlobalSearchScope;
45 import com.intellij.psi.search.searches.ReferencesSearch;
46 import com.intellij.psi.util.PsiTreeUtil;
47 import com.intellij.uiDesigner.FormEditingUtil;
48 import com.intellij.uiDesigner.StringDescriptorManager;
49 import com.intellij.uiDesigner.UIDesignerBundle;
50 import com.intellij.uiDesigner.binding.FormReferenceProvider;
51 import com.intellij.uiDesigner.compiler.AsmCodeGenerator;
52 import com.intellij.uiDesigner.designSurface.GuiEditor;
53 import com.intellij.uiDesigner.lw.StringDescriptor;
54 import com.intellij.util.ArrayUtil;
55 import com.intellij.util.IncorrectOperationException;
56 import org.jetbrains.annotations.NonNls;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59
60 import javax.swing.*;
61 import java.awt.*;
62 import java.awt.event.ActionEvent;
63 import java.awt.event.ActionListener;
64 import java.awt.event.InputEvent;
65 import java.util.*;
66
67 /**
68  * @author Anton Katilin
69  * @author Vladimir Kondratyev
70  */
71 public final class StringEditorDialog extends DialogWrapper{
72   private static final Logger LOG = Logger.getInstance("#com.intellij.uiDesigner.propertyInspector.editors.string.StringEditorDialog");
73
74   @NonNls private static final String CARD_STRING = "string";
75   @NonNls private static final String CARD_BUNDLE = "bundle";
76
77   private final GuiEditor myEditor;
78   /** Descriptor to be edited */
79   private StringDescriptor myValue;
80   private final MyForm myForm;
81   private final Locale myLocale;
82   private boolean myDefaultBundleInitialized = false;
83
84   StringEditorDialog(final Component parent,
85                      final StringDescriptor descriptor,
86                      @Nullable Locale locale,
87                      final GuiEditor editor) {
88     super(parent, true);
89     myLocale = locale;
90
91     myEditor = editor;
92
93     myForm = new MyForm();
94     setTitle(UIDesignerBundle.message("title.edit.text"));
95     setValue(descriptor);
96
97     init(); /* run initialization proc */
98   }
99
100   protected String getDimensionServiceKey() {
101     return getClass().getName();
102   }
103
104   public JComponent getPreferredFocusedComponent() {
105     if(myForm.myRbString.isSelected()){
106       return myForm.myTfValue;
107     }
108     else{
109       return super.getPreferredFocusedComponent();
110     }
111   }
112
113   @Override protected void doOKAction() {
114     if (myForm.myRbResourceBundle.isSelected()) {
115       final StringDescriptor descriptor = getDescriptor();
116       if (descriptor != null && descriptor.getKey().length() > 0) {
117         final String value = myForm.myTfRbValue.getText();
118         final PropertiesFile propFile = getPropertiesFile(descriptor);
119         if (propFile != null && propFile.findPropertyByKey(descriptor.getKey()) == null) {
120           saveCreatedProperty(propFile, descriptor.getKey(), value, myEditor.getPsiFile());
121         }
122         else {
123           final String newKeyName = saveModifiedPropertyValue(myEditor.getModule(), descriptor, myLocale, value, myEditor.getPsiFile());
124           if (newKeyName != null) {
125             myForm.myTfKey.setText(newKeyName);
126           }
127         }
128       }
129     }
130     super.doOKAction();
131   }
132
133   private PropertiesFile getPropertiesFile(final StringDescriptor descriptor) {
134     final PropertiesReferenceManager manager = PropertiesReferenceManager.getInstance(myEditor.getProject());
135     return manager.findPropertiesFile(myEditor.getModule(), descriptor.getDottedBundleName(), myLocale);
136   }
137
138   @Nullable
139   public static String saveModifiedPropertyValue(final Module module, final StringDescriptor descriptor,
140                                                  final Locale locale, final String editedValue, final PsiFile formFile) {
141     final PropertiesReferenceManager manager = PropertiesReferenceManager.getInstance(module.getProject());
142     final PropertiesFile propFile = manager.findPropertiesFile(module, descriptor.getDottedBundleName(), locale);
143     if (propFile != null) {
144       final IProperty propertyByKey = propFile.findPropertyByKey(descriptor.getKey());
145       if (propertyByKey instanceof Property && !editedValue.equals(propertyByKey.getValue())) {
146         final Collection<PsiReference> references = findPropertyReferences((Property)propertyByKey, module);
147
148         String newKeyName = null;
149         if (references.size() > 1) {
150           final int rc = Messages.showYesNoCancelDialog(module.getProject(), UIDesignerBundle.message("edit.text.multiple.usages",
151                                                                                            propertyByKey.getUnescapedKey(), references.size()),
152                                              UIDesignerBundle.message("edit.text.multiple.usages.title"),
153                                                UIDesignerBundle.message("edit.text.change.all"),
154                                                UIDesignerBundle.message("edit.text.make.unique"),
155                                                CommonBundle.getCancelButtonText(),
156                                              Messages.getWarningIcon());
157           if (rc == Messages.CANCEL) {
158             return null;
159           }
160           if (rc == Messages.NO) {
161             newKeyName = promptNewKeyName(module.getProject(), propFile, descriptor.getKey());
162             if (newKeyName == null) return null;
163           }
164         }
165         final ReadonlyStatusHandler.OperationStatus operationStatus =
166           ReadonlyStatusHandler.getInstance(module.getProject()).ensureFilesWritable(propFile.getVirtualFile());
167         if (operationStatus.hasReadonlyFiles()) {
168           return null;
169         }
170         final String newKeyName1 = newKeyName;
171         CommandProcessor.getInstance().executeCommand(
172           module.getProject(),
173           () -> {
174             UndoUtil.markPsiFileForUndo(formFile);
175             ApplicationManager.getApplication().runWriteAction(() -> {
176               PsiDocumentManager.getInstance(module.getProject()).commitAllDocuments();
177               try {
178                 if (newKeyName1 != null) {
179                   propFile.addProperty(newKeyName1, editedValue);
180                 }
181                 else {
182                   final IProperty propertyByKey1 = propFile.findPropertyByKey(descriptor.getKey());
183                   if (propertyByKey1 != null) {
184                     propertyByKey1.setValue(editedValue);
185                   }
186                 }
187               }
188               catch (IncorrectOperationException e) {
189                 LOG.error(e);
190               }
191             });
192           }, UIDesignerBundle.message("command.update.property"), null);
193         return newKeyName;
194       }
195     }
196     return null;
197   }
198
199   private static Collection<PsiReference> findPropertyReferences(final Property property, final Module module) {
200     final Collection<PsiReference> references = Collections.synchronizedList(new ArrayList<PsiReference>());
201     ProgressManager.getInstance().runProcessWithProgressSynchronously(
202       (Runnable)() -> ReferencesSearch.search(property).forEach(psiReference -> {
203         PsiMethod method = PsiTreeUtil.getParentOfType(psiReference.getElement(), PsiMethod.class);
204         if (method == null || !AsmCodeGenerator.SETUP_METHOD_NAME.equals(method.getName())) {
205           references.add(psiReference);
206         }
207         return true;
208       }), UIDesignerBundle.message("edit.text.searching.references"), false, module.getProject()
209     );
210     return references;
211   }
212
213   private static String promptNewKeyName(final Project project, final PropertiesFile propFile, final String key) {
214     String newName;
215     int index = 0;
216     do {
217       index++;
218       newName = key + index;
219     } while(propFile.findPropertyByKey(newName) != null);
220
221     InputValidator validator = new InputValidator() {
222       public boolean checkInput(String inputString) {
223         return inputString.length() > 0 && propFile.findPropertyByKey(inputString) == null;
224       }
225
226       public boolean canClose(String inputString) {
227         return checkInput(inputString);
228       }
229     };
230     return Messages.showInputDialog(project, UIDesignerBundle.message("edit.text.unique.key.prompt"),
231                                     UIDesignerBundle.message("edit.text.multiple.usages.title"),
232                                     Messages.getQuestionIcon(), newName, validator);
233   }
234
235   public static boolean saveCreatedProperty(final PropertiesFile bundle, final String name, final String value,
236                                             final PsiFile formFile) {
237     final ReadonlyStatusHandler.OperationStatus operationStatus =
238       ReadonlyStatusHandler.getInstance(bundle.getProject()).ensureFilesWritable(bundle.getVirtualFile());
239     if (operationStatus.hasReadonlyFiles()) {
240       return false;
241     }
242     CommandProcessor.getInstance().executeCommand(
243       bundle.getProject(),
244       () -> {
245         UndoUtil.markPsiFileForUndo(formFile);
246         ApplicationManager.getApplication().runWriteAction(() -> {
247           try {
248             bundle.addProperty(name, value);
249           }
250           catch (IncorrectOperationException e1) {
251             LOG.error(e1);
252           }
253         });
254       }, UIDesignerBundle.message("command.create.property"), null);
255     return true;
256   }
257
258   /**
259    * @return edited descriptor. If initial descriptor was {@code null}
260    * and user didn't change anything then this method returns {@code null}.
261    */
262   @Nullable
263   StringDescriptor getDescriptor(){
264     if(myForm.myRbString.isSelected()){ // plain value
265       final String value = myForm.myTfValue.getText();
266       if(myValue == null && value.length() == 0){
267         return null;
268       }
269       else{
270         final StringDescriptor stringDescriptor = StringDescriptor.create(value);
271         stringDescriptor.setNoI18n(myForm.myNoI18nCheckbox.isSelected());
272         return stringDescriptor;
273       }
274     }
275     else{ // bundled value
276       final String bundleName = myForm.myTfBundleName.getText();
277       final String key = myForm.myTfKey.getText();
278       return new StringDescriptor(bundleName, key);
279     }
280   }
281
282   /**
283    * Applies specified descriptor to the proper card
284    */
285   private void setValue(final StringDescriptor descriptor){
286     myValue = descriptor;
287     final CardLayout cardLayout = (CardLayout)myForm.myCardHolder.getLayout();
288     if(descriptor == null || descriptor.getValue() != null){ // trivial descriptor
289       myForm.myRbString.setSelected(true);
290       myForm.showStringDescriptor(descriptor);
291       cardLayout.show(myForm.myCardHolder, CARD_STRING);
292     }
293     else{ // bundled property
294       myForm.myRbResourceBundle.setSelected(true);
295       myForm.showResourceBundleDescriptor(descriptor);
296       cardLayout.show(myForm.myCardHolder, CARD_BUNDLE);
297     }
298   }
299
300   protected JComponent createCenterPanel() {
301     return myForm.myPanel;
302   }
303
304   private final class MyForm{
305     private JRadioButton myRbString;
306     private JRadioButton myRbResourceBundle;
307     private JPanel myCardHolder;
308     private JPanel myPanel;
309     private JTextArea myTfValue;
310     private JCheckBox myNoI18nCheckbox;
311     private TextFieldWithBrowseButton myTfBundleName;
312     private TextFieldWithBrowseButton myTfKey;
313     private JTextField myTfRbValue;
314     private JLabel myLblKey;
315     private JLabel myLblBundleName;
316
317     public MyForm() {
318       myRbString.addActionListener(
319         new ActionListener() {
320           public void actionPerformed(final ActionEvent e) {
321             CardLayout cardLayout = (CardLayout) myCardHolder.getLayout();
322             cardLayout.show(myCardHolder, CARD_STRING);
323           }
324         }
325       );
326
327       myRbResourceBundle.addActionListener(
328         new ActionListener() {
329           public void actionPerformed(final ActionEvent e) {
330             if (!myDefaultBundleInitialized) {
331               myDefaultBundleInitialized = true;
332               Set<String> bundleNames = FormEditingUtil.collectUsedBundleNames(myEditor.getRootContainer());
333               if (bundleNames.size() > 0) {
334                 myTfBundleName.setText(ArrayUtil.toStringArray(bundleNames)[0]);
335               }
336             }
337             CardLayout cardLayout = (CardLayout) myCardHolder.getLayout();
338             cardLayout.show(myCardHolder, CARD_BUNDLE);
339           }
340         }
341       );
342
343       setupResourceBundleCard();
344     }
345
346     private void setupResourceBundleCard() {
347       // Enable keyboard pressing
348       myTfBundleName.registerKeyboardAction(
349         new AbstractAction() {
350           public void actionPerformed(final ActionEvent e) {
351             myTfBundleName.getButton().doClick();
352           }
353         },
354         KeyStroke.getKeyStroke(myLblBundleName.getDisplayedMnemonic(), InputEvent.ALT_DOWN_MASK),
355         JComponent.WHEN_IN_FOCUSED_WINDOW
356       );
357
358       myTfBundleName.addActionListener(
359         new ActionListener() {
360           public void actionPerformed(final ActionEvent e) {
361             Project project = myEditor.getProject();
362             final String bundleNameText = myTfBundleName.getText().replace('/', '.');
363             PropertiesFile file = PropertiesUtilBase.getPropertiesFile(bundleNameText, myEditor.getModule(), myLocale);
364             PsiFile initialPropertiesFile = file == null ? null : file.getContainingFile();
365             final GlobalSearchScope moduleScope = GlobalSearchScope.moduleWithDependenciesScope(myEditor.getModule());
366             TreeFileChooser fileChooser = TreeClassChooserFactory.getInstance(project).createFileChooser(UIDesignerBundle.message("title.choose.properties.file"), initialPropertiesFile,
367                                                                                                          StdFileTypes.PROPERTIES, new TreeFileChooser.PsiFileFilter() {
368               public boolean accept(PsiFile file) {
369                 final VirtualFile virtualFile = file.getVirtualFile();
370                 return virtualFile != null && moduleScope.contains(virtualFile);
371               }
372             });
373             fileChooser.showDialog();
374             PropertiesFile propertiesFile = (PropertiesFile)fileChooser.getSelectedFile();
375             if (propertiesFile == null) {
376               return;
377             }
378             final String bundleName = FormReferenceProvider.getBundleName(propertiesFile);
379             if (bundleName == null) {
380               return;
381             }
382             myTfBundleName.setText(bundleName);
383           }
384         }
385       );
386
387       // Enable keyboard pressing
388       myTfKey.registerKeyboardAction(
389         new AbstractAction() {
390           public void actionPerformed(final ActionEvent e) {
391             myTfKey.getButton().doClick();
392           }
393         },
394         KeyStroke.getKeyStroke(myLblKey.getDisplayedMnemonic(), InputEvent.ALT_DOWN_MASK),
395         JComponent.WHEN_IN_FOCUSED_WINDOW
396       );
397
398       myTfKey.addActionListener(
399         new ActionListener() {
400           public void actionPerformed(final ActionEvent e) {
401             // 1. Check that bundle exist. Otherwise we cannot show key chooser
402             final String bundleName = myTfBundleName.getText();
403             if (bundleName.length() == 0) {
404               Messages.showErrorDialog(
405                 UIDesignerBundle.message("error.specify.bundle.name"),
406                 CommonBundle.getErrorTitle()
407               );
408               return;
409             }
410             final PropertiesReferenceManager manager = PropertiesReferenceManager.getInstance(myEditor.getProject());
411             final PropertiesFile bundle = manager.findPropertiesFile(myEditor.getModule(), bundleName.replace('/', '.'), myLocale);
412             if (bundle == null) {
413               Messages.showErrorDialog(
414                 UIDesignerBundle.message("error.bundle.does.not.exist", bundleName),
415                 CommonBundle.getErrorTitle()
416               );
417               return;
418             }
419
420             // 2. Show key chooser
421             final KeyChooserDialog dialog = new KeyChooserDialog(
422               myTfKey,
423               bundle,
424               bundleName,
425               myTfKey.getText(), // key to preselect
426               myEditor
427             );
428             if (!dialog.showAndGet()) {
429               return;
430             }
431
432             // 3. Apply new key/value
433             final StringDescriptor descriptor = dialog.getDescriptor();
434             if (descriptor == null) {
435               return;
436             }
437             myTfKey.setText(descriptor.getKey());
438             myTfRbValue.setText(descriptor.getResolvedValue());
439           }
440         }
441       );
442     }
443
444     public void showStringDescriptor(@Nullable final StringDescriptor descriptor) {
445       myTfValue.setText(StringDescriptorManager.getInstance(myEditor.getModule()).resolve(descriptor, myLocale));
446       myNoI18nCheckbox.setSelected(descriptor != null && descriptor.isNoI18n());
447     }
448
449     public void showResourceBundleDescriptor(@NotNull final StringDescriptor descriptor) {
450       final String key = descriptor.getKey();
451       LOG.assertTrue(key != null);
452       myTfBundleName.setText(descriptor.getBundleName());
453       myTfKey.setText(key);
454       myTfRbValue.setText(StringDescriptorManager.getInstance(myEditor.getModule()).resolve(descriptor, myLocale));
455     }
456   }
457 }