cbd5201134ea9280c50497e5b05eac65c0301be1
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / ui / ComponentWithBrowseButton.java
1 /*
2  * Copyright 2000-2015 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.Disposer;
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.ui.GuiUtils;
36 import com.intellij.ui.UIBundle;
37 import com.intellij.util.ui.UIUtil;
38 import com.intellij.util.ui.accessibility.ScreenReader;
39 import com.intellij.util.ui.update.LazyUiDisposable;
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     final Comp comp = getChildComponent();
96     Dimension size = GuiUtils.getSizeByChars(charCount, comp);
97     comp.setPreferredSize(size);
98     final Dimension preferredSize = myBrowseButton.getPreferredSize();
99     setPreferredSize(new Dimension(size.width + preferredSize.width + 2, UIUtil.isUnderAquaLookAndFeel() ? preferredSize.height : preferredSize.height + 2));
100   }
101
102   @Override
103   public void setEnabled(boolean enabled) {
104     super.setEnabled(enabled);
105     myBrowseButton.setEnabled(enabled && myButtonEnabled);
106     myComponent.setEnabled(enabled);
107   }
108
109   public void setButtonEnabled(boolean buttonEnabled) {
110     myButtonEnabled = buttonEnabled;
111     setEnabled(isEnabled());
112   }
113
114   public void setButtonIcon(Icon icon) {
115     myBrowseButton.setIcon(icon);
116   }
117
118   /**
119    * Adds specified <code>listener</code> to the browse button.
120    */
121   public void addActionListener(ActionListener listener){
122     myBrowseButton.addActionListener(listener);
123   }
124
125   public void removeActionListener(ActionListener listener) {
126     myBrowseButton.removeActionListener(listener);
127   }
128
129   public void addBrowseFolderListener(@Nullable @Nls(capitalization = Nls.Capitalization.Title) String title,
130                                       @Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String description,
131                                       @Nullable Project project,
132                                       FileChooserDescriptor fileChooserDescriptor,
133                                       TextComponentAccessor<Comp> accessor) {
134     addBrowseFolderListener(title, description, project, fileChooserDescriptor, accessor, true);
135   }
136
137   public void addBrowseFolderListener(@Nullable @Nls(capitalization = Nls.Capitalization.Title) String title,
138                                       @Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String description,
139                                       @Nullable Project project,
140                                       FileChooserDescriptor fileChooserDescriptor,
141                                       TextComponentAccessor<Comp> accessor, boolean autoRemoveOnHide) {
142     addBrowseFolderListener(project, new BrowseFolderActionListener<>(title, description, this, project, fileChooserDescriptor, accessor), autoRemoveOnHide);
143   }
144
145   public void addBrowseFolderListener(@Nullable Project project, final BrowseFolderActionListener<Comp> actionListener) {
146     addBrowseFolderListener(project, actionListener, true);
147   }
148
149   public void addBrowseFolderListener(@Nullable Project project, final BrowseFolderActionListener<Comp> actionListener, boolean autoRemoveOnHide) {
150     if (autoRemoveOnHide) {
151       new LazyUiDisposable<ComponentWithBrowseButton<Comp>>(null, this, this) {
152         @Override
153         protected void initialize(@NotNull Disposable parent, @NotNull ComponentWithBrowseButton<Comp> child, @Nullable Project project) {
154           addActionListener(actionListener);
155           Disposer.register(child, new Disposable() {
156             @Override
157             public void dispose() {
158               removeActionListener(actionListener);
159             }
160           });
161         }
162       };
163     } else {
164       addActionListener(actionListener);
165     }
166   }
167
168   @Override
169   public void dispose() { }
170
171   public FixedSizeButton getButton() {
172     return myBrowseButton;
173   }
174
175   /**
176    * Do not use this class directly it is public just to hack other implementation of controls similar to TextFieldWithBrowseButton.
177    */
178   public static final class MyDoClickAction extends DumbAwareAction {
179     private final FixedSizeButton myBrowseButton;
180     public MyDoClickAction(FixedSizeButton browseButton) {
181       myBrowseButton = browseButton;
182     }
183
184     @Override
185     public void update(AnActionEvent e) {
186       e.getPresentation().setEnabled(myBrowseButton.isVisible() && myBrowseButton.isEnabled());
187     }
188
189     @Override
190     public void actionPerformed(AnActionEvent e){
191       myBrowseButton.doClick();
192     }
193
194     public void registerShortcut(JComponent textField) {
195       ShortcutSet shiftEnter = new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK));
196       registerCustomShortcutSet(shiftEnter, textField);
197       myBrowseButton.setToolTipText(KeymapUtil.getShortcutsText(shiftEnter.getShortcuts()));
198     }
199
200     public static void addTo(FixedSizeButton browseButton, JComponent aComponent) {
201       new MyDoClickAction(browseButton).registerShortcut(aComponent);
202     }
203   }
204
205   public static class BrowseFolderActionListener<T extends JComponent> implements ActionListener {
206     private final String myTitle;
207     private final String myDescription;
208     protected ComponentWithBrowseButton<T> myTextComponent;
209     private final TextComponentAccessor<T> myAccessor;
210     private Project myProject;
211     protected final FileChooserDescriptor myFileChooserDescriptor;
212
213     public BrowseFolderActionListener(@Nullable @Nls(capitalization = Nls.Capitalization.Title) String title,
214                                       @Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String description,
215                                       ComponentWithBrowseButton<T> textField,
216                                       @Nullable Project project,
217                                       FileChooserDescriptor fileChooserDescriptor,
218                                       TextComponentAccessor<T> accessor) {
219       if (fileChooserDescriptor != null && fileChooserDescriptor.isChooseMultiple()) {
220         LOG.error("multiple selection not supported");
221         fileChooserDescriptor = new FileChooserDescriptor(fileChooserDescriptor) {
222           @Override
223           public boolean isChooseMultiple() {
224             return false;
225           }
226         };
227       }
228
229       myTitle = title;
230       myDescription = description;
231       myTextComponent = textField;
232       myProject = project;
233       myFileChooserDescriptor = fileChooserDescriptor;
234       myAccessor = accessor;
235     }
236
237     @Nullable
238     protected Project getProject() {
239       return myProject;
240     }
241
242     protected void setProject(@Nullable Project project) {
243       myProject = project;
244     }
245
246     @Override
247     public void actionPerformed(ActionEvent e) {
248       FileChooserDescriptor fileChooserDescriptor = myFileChooserDescriptor;
249       if (myTitle != null || myDescription != null) {
250         fileChooserDescriptor = (FileChooserDescriptor)myFileChooserDescriptor.clone();
251         if (myTitle != null) {
252           fileChooserDescriptor.setTitle(myTitle);
253         }
254         if (myDescription != null) {
255           fileChooserDescriptor.setDescription(myDescription);
256         }
257       }
258
259       FileChooser.chooseFile(fileChooserDescriptor, getProject(), myTextComponent, getInitialFile(), this::onFileChosen);
260     }
261
262     @Nullable
263     protected VirtualFile getInitialFile() {
264       String directoryName = getComponentText();
265       if (StringUtil.isEmptyOrSpaces(directoryName)) {
266         return null;
267       }
268
269       directoryName = FileUtil.toSystemIndependentName(directoryName);
270       VirtualFile path = LocalFileSystem.getInstance().findFileByPath(expandPath(directoryName));
271       while (path == null && directoryName.length() > 0) {
272         int pos = directoryName.lastIndexOf('/');
273         if (pos <= 0) break;
274         directoryName = directoryName.substring(0, pos);
275         path = LocalFileSystem.getInstance().findFileByPath(directoryName);
276       }
277       return path;
278     }
279
280     @NotNull
281     protected String expandPath(@NotNull String path) {
282       return path;
283     }
284
285     protected String getComponentText() {
286       return myAccessor.getText(myTextComponent.getChildComponent()).trim();
287     }
288
289     @NotNull
290     protected String chosenFileToResultingText(@NotNull VirtualFile chosenFile) {
291       return chosenFile.getPresentableUrl();
292     }
293
294     protected void onFileChosen(@NotNull VirtualFile chosenFile) {
295       myAccessor.setText(myTextComponent.getChildComponent(), chosenFileToResultingText(chosenFile));
296     }
297   }
298
299   @Override
300   public final void requestFocus() {
301     myComponent.requestFocus();
302   }
303
304   @SuppressWarnings("deprecation")
305   @Override
306   public final void setNextFocusableComponent(Component aComponent) {
307     super.setNextFocusableComponent(aComponent);
308     myComponent.setNextFocusableComponent(aComponent);
309   }
310
311   private KeyEvent myCurrentEvent = null;
312
313   @Override
314   protected final boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
315     if (condition == WHEN_FOCUSED && myCurrentEvent != e) {
316       try {
317         myCurrentEvent = e;
318         myComponent.dispatchEvent(e);
319       }
320       finally {
321         myCurrentEvent = null;
322       }
323     }
324     if (e.isConsumed()) return true;
325     return super.processKeyBinding(ks, e, condition, pressed);
326   }
327 }