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