6cd7bc7379debb8b7df420f7f3c4b012193baeb4
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / ui / ComponentWithBrowseButton.java
1 /*
2  * Copyright 2000-2017 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.openapi.ui;
17
18 import com.intellij.openapi.Disposable;
19 import com.intellij.openapi.actionSystem.AnActionEvent;
20 import com.intellij.openapi.actionSystem.CustomShortcutSet;
21 import com.intellij.openapi.actionSystem.ShortcutSet;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.fileChooser.FileChooser;
25 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
26 import com.intellij.openapi.keymap.KeymapUtil;
27 import com.intellij.openapi.project.DumbAwareAction;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.util.IconLoader;
30 import com.intellij.openapi.util.SystemInfo;
31 import com.intellij.openapi.util.io.FileUtil;
32 import com.intellij.openapi.util.text.StringUtil;
33 import com.intellij.openapi.vfs.LocalFileSystem;
34 import com.intellij.openapi.vfs.VirtualFile;
35 import com.intellij.openapi.wm.IdeFocusManager;
36 import com.intellij.ui.GuiUtils;
37 import com.intellij.ui.UIBundle;
38 import com.intellij.util.ui.UIUtil;
39 import com.intellij.util.ui.accessibility.ScreenReader;
40 import org.jetbrains.annotations.Nls;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import javax.swing.*;
45 import java.awt.*;
46 import java.awt.event.ActionEvent;
47 import java.awt.event.ActionListener;
48 import java.awt.event.InputEvent;
49 import java.awt.event.KeyEvent;
50
51 public class ComponentWithBrowseButton<Comp extends JComponent> extends JPanel implements Disposable {
52   private static final Logger LOG = Logger.getInstance(ComponentWithBrowseButton.class);
53
54   private final Comp myComponent;
55   private final FixedSizeButton myBrowseButton;
56   private boolean myButtonEnabled = true;
57
58   public ComponentWithBrowseButton(Comp component, @Nullable ActionListener browseActionListener) {
59     super(new BorderLayout(SystemInfo.isMac ? 0 : 2, 0));
60
61     myComponent = component;
62     // required! otherwise JPanel will occasionally gain focus instead of the component
63     setFocusable(false);
64     add(myComponent, BorderLayout.CENTER);
65
66     myBrowseButton = new FixedSizeButton(myComponent);
67     if (browseActionListener != null) {
68       myBrowseButton.addActionListener(browseActionListener);
69     }
70     add(centerComponentVertically(myBrowseButton), BorderLayout.EAST);
71
72     myBrowseButton.setToolTipText(UIBundle.message("component.with.browse.button.browse.button.tooltip.text"));
73     // FixedSizeButton isn't focusable but it should be selectable via keyboard.
74     if (ApplicationManager.getApplication() != null) {  // avoid crash at design time
75       new MyDoClickAction(myBrowseButton).registerShortcut(myComponent);
76     }
77     if (ScreenReader.isActive()) {
78       myBrowseButton.setFocusable(true);
79       myBrowseButton.getAccessibleContext().setAccessibleName("Browse");
80     }
81   }
82
83   @NotNull
84   private static JPanel centerComponentVertically(@NotNull Component component) {
85     JPanel panel = new JPanel(new GridBagLayout());
86     panel.add(component, new GridBagConstraints());
87     return panel;
88   }
89
90   public final Comp getChildComponent() {
91     return myComponent;
92   }
93
94   public void setTextFieldPreferredWidth(final int charCount) {
95     JComponent comp = getChildComponent();
96     Dimension size = GuiUtils.getSizeByChars(charCount, comp);
97     comp.setPreferredSize(size);
98     Dimension preferredSize = myBrowseButton.getPreferredSize();
99
100     boolean keepHeight = UIUtil.isUnderAquaLookAndFeel() || UIUtil.isUnderWin10LookAndFeel();
101     preferredSize.setSize(size.width + preferredSize.width + 2,
102                           keepHeight ? preferredSize.height : preferredSize.height + 2);
103
104     setPreferredSize(preferredSize);
105   }
106
107   @Override
108   public void setEnabled(boolean enabled) {
109     super.setEnabled(enabled);
110     myBrowseButton.setEnabled(enabled && myButtonEnabled);
111     myComponent.setEnabled(enabled);
112   }
113
114   public void setButtonEnabled(boolean buttonEnabled) {
115     myButtonEnabled = buttonEnabled;
116     setEnabled(isEnabled());
117   }
118
119   public void setButtonIcon(Icon icon) {
120     myBrowseButton.setIcon(icon);
121     myBrowseButton.setDisabledIcon(IconLoader.getDisabledIcon(icon));
122   }
123
124   /**
125    * Adds specified <code>listener</code> to the browse button.
126    */
127   public void addActionListener(ActionListener listener){
128     myBrowseButton.addActionListener(listener);
129   }
130
131   public void removeActionListener(ActionListener listener) {
132     myBrowseButton.removeActionListener(listener);
133   }
134
135   public void addBrowseFolderListener(@Nullable @Nls(capitalization = Nls.Capitalization.Title) String title,
136                                       @Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String description,
137                                       @Nullable Project project,
138                                       FileChooserDescriptor fileChooserDescriptor,
139                                       TextComponentAccessor<Comp> accessor) {
140     addActionListener(new BrowseFolderActionListener<>(title, description, this, project, fileChooserDescriptor, accessor));
141   }
142
143   /**
144    * @deprecated use {@link #addBrowseFolderListener(String, String, Project, FileChooserDescriptor, TextComponentAccessor)} instead
145    */
146   @Deprecated
147   public void addBrowseFolderListener(@Nullable @Nls(capitalization = Nls.Capitalization.Title) String title,
148                                       @Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String description,
149                                       @Nullable Project project,
150                                       FileChooserDescriptor fileChooserDescriptor,
151                                       TextComponentAccessor<Comp> accessor, boolean autoRemoveOnHide) {
152     addBrowseFolderListener(title, description, project, fileChooserDescriptor, accessor);
153   }
154
155   /**
156    * @deprecated use {@link #addActionListener(ActionListener)} instead
157    */
158   @Deprecated
159   @SuppressWarnings("UnusedParameters")
160   public void addBrowseFolderListener(@Nullable Project project, final BrowseFolderActionListener<Comp> actionListener) {
161     addActionListener(actionListener);
162   }
163
164   /**
165    * @deprecated use {@link #addActionListener(ActionListener)} instead
166    */
167   @Deprecated
168   @SuppressWarnings("UnusedParameters")
169   public void addBrowseFolderListener(@Nullable Project project, final BrowseFolderActionListener<Comp> actionListener, boolean autoRemoveOnHide) {
170     addActionListener(actionListener);
171   }
172
173   @Override
174   public void dispose() {
175     ActionListener[] listeners = myBrowseButton.getActionListeners();
176     for (ActionListener listener : listeners) {
177       myBrowseButton.removeActionListener(listener);
178     }
179   }
180
181   public FixedSizeButton getButton() {
182     return myBrowseButton;
183   }
184
185   /**
186    * Do not use this class directly it is public just to hack other implementation of controls similar to TextFieldWithBrowseButton.
187    */
188   public static final class MyDoClickAction extends DumbAwareAction {
189     private final FixedSizeButton myBrowseButton;
190     public MyDoClickAction(FixedSizeButton browseButton) {
191       myBrowseButton = browseButton;
192     }
193
194     @Override
195     public void update(AnActionEvent e) {
196       e.getPresentation().setEnabled(myBrowseButton.isVisible() && myBrowseButton.isEnabled());
197     }
198
199     @Override
200     public void actionPerformed(AnActionEvent e){
201       myBrowseButton.doClick();
202     }
203
204     public void registerShortcut(JComponent textField) {
205       ShortcutSet shiftEnter = new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK));
206       registerCustomShortcutSet(shiftEnter, textField);
207       myBrowseButton.setToolTipText(KeymapUtil.getShortcutsText(shiftEnter.getShortcuts()));
208     }
209
210     public static void addTo(FixedSizeButton browseButton, JComponent aComponent) {
211       new MyDoClickAction(browseButton).registerShortcut(aComponent);
212     }
213   }
214
215   public static class BrowseFolderActionListener<T extends JComponent> implements ActionListener {
216     private final String myTitle;
217     private final String myDescription;
218     protected ComponentWithBrowseButton<T> myTextComponent;
219     private final TextComponentAccessor<T> myAccessor;
220     private Project myProject;
221     protected final FileChooserDescriptor myFileChooserDescriptor;
222
223     public BrowseFolderActionListener(@Nullable @Nls(capitalization = Nls.Capitalization.Title) String title,
224                                       @Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String description,
225                                       ComponentWithBrowseButton<T> textField,
226                                       @Nullable Project project,
227                                       FileChooserDescriptor fileChooserDescriptor,
228                                       TextComponentAccessor<T> accessor) {
229       if (fileChooserDescriptor != null && fileChooserDescriptor.isChooseMultiple()) {
230         LOG.error("multiple selection not supported");
231         fileChooserDescriptor = new FileChooserDescriptor(fileChooserDescriptor) {
232           @Override
233           public boolean isChooseMultiple() {
234             return false;
235           }
236         };
237       }
238
239       myTitle = title;
240       myDescription = description;
241       myTextComponent = textField;
242       myProject = project;
243       myFileChooserDescriptor = fileChooserDescriptor;
244       myAccessor = accessor;
245     }
246
247     @Nullable
248     protected Project getProject() {
249       return myProject;
250     }
251
252     protected void setProject(@Nullable Project project) {
253       myProject = project;
254     }
255
256     @Override
257     public void actionPerformed(ActionEvent e) {
258       FileChooserDescriptor fileChooserDescriptor = myFileChooserDescriptor;
259       if (myTitle != null || myDescription != null) {
260         fileChooserDescriptor = (FileChooserDescriptor)myFileChooserDescriptor.clone();
261         if (myTitle != null) {
262           fileChooserDescriptor.setTitle(myTitle);
263         }
264         if (myDescription != null) {
265           fileChooserDescriptor.setDescription(myDescription);
266         }
267       }
268
269       FileChooser.chooseFile(fileChooserDescriptor, getProject(), myTextComponent, getInitialFile(), this::onFileChosen);
270     }
271
272     @Nullable
273     protected VirtualFile getInitialFile() {
274       String directoryName = getComponentText();
275       if (StringUtil.isEmptyOrSpaces(directoryName)) {
276         return null;
277       }
278
279       directoryName = FileUtil.toSystemIndependentName(directoryName);
280       VirtualFile path = LocalFileSystem.getInstance().findFileByPath(expandPath(directoryName));
281       while (path == null && directoryName.length() > 0) {
282         int pos = directoryName.lastIndexOf('/');
283         if (pos <= 0) break;
284         directoryName = directoryName.substring(0, pos);
285         path = LocalFileSystem.getInstance().findFileByPath(directoryName);
286       }
287       return path;
288     }
289
290     @NotNull
291     protected String expandPath(@NotNull String path) {
292       return path;
293     }
294
295     protected String getComponentText() {
296       return myAccessor.getText(myTextComponent.getChildComponent()).trim();
297     }
298
299     @NotNull
300     protected String chosenFileToResultingText(@NotNull VirtualFile chosenFile) {
301       return chosenFile.getPresentableUrl();
302     }
303
304     protected void onFileChosen(@NotNull VirtualFile chosenFile) {
305       myAccessor.setText(myTextComponent.getChildComponent(), chosenFileToResultingText(chosenFile));
306     }
307   }
308
309   @Override
310   public final void requestFocus() {
311     IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() ->
312       IdeFocusManager.getGlobalInstance().requestFocus(myComponent, true));
313   }
314
315   @SuppressWarnings("deprecation")
316   @Override
317   public final void setNextFocusableComponent(Component aComponent) {
318     super.setNextFocusableComponent(aComponent);
319     myComponent.setNextFocusableComponent(aComponent);
320   }
321
322   private KeyEvent myCurrentEvent = null;
323
324   @Override
325   protected final boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
326     if (condition == WHEN_FOCUSED && myCurrentEvent != e) {
327       try {
328         myCurrentEvent = e;
329         myComponent.dispatchEvent(e);
330       }
331       finally {
332         myCurrentEvent = null;
333       }
334     }
335     if (e.isConsumed()) return true;
336     return super.processKeyBinding(ks, e, condition, pressed);
337   }
338 }