View Javadoc
1   /*
2    * Copyright (c) 2020 bahlef.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v2.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v20.html
7    * Contributors:
8    * bahlef - initial API and implementation and/or initial documentation
9    */
10  
11  package de.funfried.netbeans.plugins.external.formatter.javascript.eclipse.ui;
12  
13  import java.awt.Color;
14  import java.awt.event.ActionEvent;
15  import java.awt.event.ActionListener;
16  import java.awt.event.FocusAdapter;
17  import java.awt.event.FocusEvent;
18  import java.awt.event.ItemEvent;
19  import java.awt.event.ItemListener;
20  import java.io.File;
21  import java.io.IOException;
22  import java.net.URL;
23  import java.nio.file.Path;
24  import java.nio.file.Paths;
25  import java.util.List;
26  import java.util.Objects;
27  import java.util.logging.Level;
28  import java.util.logging.Logger;
29  import java.util.prefs.Preferences;
30  
31  import javax.swing.DefaultComboBoxModel;
32  import javax.swing.GroupLayout;
33  import javax.swing.JButton;
34  import javax.swing.JCheckBox;
35  import javax.swing.JComboBox;
36  import javax.swing.JLabel;
37  import javax.swing.JTextField;
38  import javax.swing.LayoutStyle;
39  import javax.swing.SwingConstants;
40  import javax.swing.event.DocumentEvent;
41  import javax.swing.event.DocumentListener;
42  import javax.swing.filechooser.FileFilter;
43  import javax.swing.filechooser.FileNameExtensionFilter;
44  
45  import org.apache.commons.lang3.StringUtils;
46  import org.netbeans.api.project.Project;
47  import org.openide.awt.Mnemonics;
48  import org.openide.filesystems.FileChooserBuilder;
49  import org.openide.util.NbBundle;
50  import org.xml.sax.SAXException;
51  
52  import de.funfried.netbeans.plugins.external.formatter.eclipse.xml.ConfigReader;
53  import de.funfried.netbeans.plugins.external.formatter.exceptions.ConfigReadException;
54  import de.funfried.netbeans.plugins.external.formatter.javascript.eclipse.EclipseJavascriptFormatterSettings;
55  import de.funfried.netbeans.plugins.external.formatter.ui.options.AbstractFormatterOptionsPanel;
56  
57  /**
58   *
59   * @author bahlef
60   */
61  public class EclipseJavascriptFormatterOptionsPanel extends AbstractFormatterOptionsPanel {
62  	/** {@link Logger} of this class. */
63  	private static final Logger log = Logger.getLogger(EclipseJavascriptFormatterOptionsPanel.class.getName());
64  
65  	/**
66  	 * Creates new form {@link EclipseJavascriptFormatterOptionsPanel}.
67  	 *
68  	 * @param project the {@link Project} if the panel is used to modify project
69  	 *        specific settings, otherwise {@code null}
70  	 */
71  	public EclipseJavascriptFormatterOptionsPanel(Project project) {
72  		super(project);
73  
74  		initComponents();
75  
76  		if (project != null) {
77  			lblFormatterFile.setToolTipText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.lblFormatterFile.toolTipText.projectSpecific"));
78  			browseButton.setToolTipText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.browseButton.toolTipText.projectSpecific"));
79  		}
80  	}
81  
82  	/**
83  	 * This method is called from within the constructor to
84  	 * initialize the form.
85  	 * WARNING: Do NOT modify this code. The content of this method is
86  	 * always regenerated by the Form Editor.
87  	 */
88  	@SuppressWarnings("unchecked")
89      // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
90      private void initComponents() {
91  
92          lblFormatterFile = new JLabel();
93          formatterLocField = new JTextField();
94          browseButton = new JButton();
95          errorLabel = new JLabel();
96          jLabel2 = new JLabel();
97          cbProfile = new JComboBox<>();
98          lblProfile = new JLabel();
99          cbUseProjectPref = new JCheckBox();
100         lblLinefeed = new JLabel();
101         cbLinefeed = new JComboBox<>();
102 
103         lblFormatterFile.setHorizontalAlignment(SwingConstants.RIGHT);
104         Mnemonics.setLocalizedText(lblFormatterFile, NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.lblFormatterFile.text")); // NOI18N
105         lblFormatterFile.setToolTipText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.lblFormatterFile.toolTipText")); // NOI18N
106 
107         formatterLocField.setText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.formatterLocField.text")); // NOI18N
108         formatterLocField.setPreferredSize(formatterLocField.getMinimumSize());
109         formatterLocField.addFocusListener(new FocusAdapter() {
110             public void focusLost(FocusEvent evt) {
111                 formatterLocFieldFocusLost(evt);
112             }
113         });
114         formatterLocField.addActionListener(new ActionListener() {
115             public void actionPerformed(ActionEvent evt) {
116                 formatterLocFieldActionPerformed(evt);
117             }
118         });
119 
120         Mnemonics.setLocalizedText(browseButton, NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.browseButton.text")); // NOI18N
121         browseButton.setToolTipText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.browseButton.toolTipText")); // NOI18N
122         browseButton.addActionListener(new ActionListener() {
123             public void actionPerformed(ActionEvent evt) {
124                 browseButtonActionPerformed(evt);
125             }
126         });
127 
128         errorLabel.setForeground(new Color(255, 51, 51));
129         Mnemonics.setLocalizedText(errorLabel, NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.errorLabel.text")); // NOI18N
130 
131         Mnemonics.setLocalizedText(jLabel2, NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.jLabel2.text")); // NOI18N
132         jLabel2.setEnabled(false);
133 
134         cbProfile.setEnabled(false);
135         cbProfile.addItemListener(new ItemListener() {
136             public void itemStateChanged(ItemEvent evt) {
137                 cbProfileItemStateChanged(evt);
138             }
139         });
140 
141         lblProfile.setHorizontalAlignment(SwingConstants.RIGHT);
142         Mnemonics.setLocalizedText(lblProfile, NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.lblProfile.text")); // NOI18N
143         lblProfile.setToolTipText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.lblProfile.toolTipText")); // NOI18N
144 
145         cbUseProjectPref.setSelected(true);
146         Mnemonics.setLocalizedText(cbUseProjectPref, NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.cbUseProjectPref.text")); // NOI18N
147         cbUseProjectPref.setToolTipText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.cbUseProjectPref.toolTipText")); // NOI18N
148 
149         lblLinefeed.setHorizontalAlignment(SwingConstants.RIGHT);
150         Mnemonics.setLocalizedText(lblLinefeed, NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.lblLinefeed.text")); // NOI18N
151         lblLinefeed.setToolTipText(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.lblLinefeed.toolTipText")); // NOI18N
152 
153         cbLinefeed.setModel(new DefaultComboBoxModel<>(new String[] { "System", "\\n", "\\r\\n", "\\r" }));
154         cbLinefeed.addItemListener(new ItemListener() {
155             public void itemStateChanged(ItemEvent evt) {
156                 cbLinefeedItemStateChanged(evt);
157             }
158         });
159 
160         GroupLayout layout = new GroupLayout(this);
161         this.setLayout(layout);
162         layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
163             .addGroup(layout.createSequentialGroup()
164                 .addContainerGap()
165                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
166                     .addComponent(lblFormatterFile)
167                     .addComponent(lblProfile, GroupLayout.PREFERRED_SIZE, 110, GroupLayout.PREFERRED_SIZE)
168                     .addComponent(lblLinefeed, GroupLayout.PREFERRED_SIZE, 110, GroupLayout.PREFERRED_SIZE))
169                 .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
170                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
171                     .addGroup(layout.createSequentialGroup()
172                         .addComponent(jLabel2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
173                         .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
174                         .addComponent(errorLabel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
175                     .addGroup(layout.createSequentialGroup()
176                         .addComponent(formatterLocField, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
177                         .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
178                         .addComponent(browseButton))
179                     .addComponent(cbLinefeed, GroupLayout.PREFERRED_SIZE, 121, GroupLayout.PREFERRED_SIZE)
180                     .addComponent(cbUseProjectPref)
181                     .addComponent(cbProfile, GroupLayout.PREFERRED_SIZE, 200, GroupLayout.PREFERRED_SIZE))
182                 .addContainerGap())
183         );
184         layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
185             .addGroup(layout.createSequentialGroup()
186                 .addContainerGap()
187                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
188                     .addComponent(lblFormatterFile)
189                     .addComponent(formatterLocField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
190                     .addComponent(browseButton))
191                 .addGap(7, 7, 7)
192                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
193                     .addComponent(errorLabel, GroupLayout.PREFERRED_SIZE, 26, GroupLayout.PREFERRED_SIZE)
194                     .addComponent(jLabel2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
195                 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
196                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
197                     .addComponent(cbProfile, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
198                     .addComponent(lblProfile))
199                 .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
200                 .addComponent(cbUseProjectPref)
201                 .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
202                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
203                     .addComponent(cbLinefeed, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
204                     .addComponent(lblLinefeed))
205                 .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
206         );
207 
208         formatterLocField.getDocument().addDocumentListener(new DocumentListener() {
209             /**
210             * {@inheritDoc}
211             */
212             @Override
213             public void insertUpdate(DocumentEvent e) {
214                 fireChangedListener();
215             }
216 
217             /**
218             * {@inheritDoc}
219             */
220             @Override
221             public void removeUpdate(DocumentEvent e) {
222                 fireChangedListener();
223             }
224 
225             /**
226             * {@inheritDoc}
227             */
228             @Override
229             public void changedUpdate(DocumentEvent e) {
230                 fireChangedListener();
231             }
232         });
233     }// </editor-fold>//GEN-END:initComponents
234 
235     private void formatterLocFieldActionPerformed(ActionEvent evt) {//GEN-FIRST:event_formatterLocFieldActionPerformed
236 		loadEclipseFormatterFileForPreview(formatterLocField.getText(), getSelectedProfile());
237 		fireChangedListener();
238     }//GEN-LAST:event_formatterLocFieldActionPerformed
239 
240     private void browseButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed
241 		// The default dir to use if no value is stored
242 		File home = new File(System.getProperty("user.home"));
243 		String formatterFilePath = formatterLocField.getText();
244 		if (StringUtils.isNotBlank(formatterFilePath)) {
245 			File formatterFile = new File(formatterFilePath);
246 			if (formatterFile.canRead()) {
247 				home = formatterFile;
248 			}
249 		}
250 
251 		final FileNameExtensionFilter fileNameExtensionFilterXML = new FileNameExtensionFilter("Eclipse formatter (*.xml)", "xml");
252 		final FileNameExtensionFilter fileNameExtensionFilterEPF = new FileNameExtensionFilter("Workspace mechanic (*.epf)", "epf");
253 		final FileFilter fileNameExtensionFilterProjectSetting = new FileFilter() {
254 			/**
255 			 * {@inheritDoc}
256 			 */
257 			@Override
258 			public boolean accept(File f) {
259 				if (f.isDirectory()) {
260 					return true;
261 				}
262 				return EclipseJavascriptFormatterSettings.PROJECT_PREF_FILE.equals(f.getName());
263 			}
264 
265 			/**
266 			 * {@inheritDoc}
267 			 */
268 			@Override
269 			public String getDescription() {
270 				return "Eclipse project settings (" + EclipseJavascriptFormatterSettings.PROJECT_PREF_FILE + ")";
271 			}
272 		};
273 		//Now build a file chooser and invoke the dialog in one line of code
274 		//"user-dir" is our unique key
275 		File toAdd = new FileChooserBuilder("user-dir").setFileHiding(false).setFilesOnly(true).setTitle("Choose configuration ...").setDefaultWorkingDirectory(home).setApproveText("Choose")
276 		  .addFileFilter(fileNameExtensionFilterProjectSetting).addFileFilter(fileNameExtensionFilterXML).addFileFilter(fileNameExtensionFilterEPF).setFileFilter(fileNameExtensionFilterXML)
277 		  .showOpenDialog();
278 		//Result will be null if the user clicked cancel or closed the dialog w/o OK
279 		if (toAdd != null) {
280 			loadEclipseFormatterFileForPreview(toAdd.getAbsolutePath(), getSelectedProfile());
281 			fireChangedListener();
282 		}
283     }//GEN-LAST:event_browseButtonActionPerformed
284 
285     private void cbProfileItemStateChanged(ItemEvent evt) {//GEN-FIRST:event_cbProfileItemStateChanged
286 		if (ItemEvent.SELECTED == evt.getStateChange()) {
287 			cbProfile.setToolTipText(Objects.toString(cbProfile.getSelectedItem(), null));
288 
289 			fireChangedListener();
290 		}
291     }//GEN-LAST:event_cbProfileItemStateChanged
292 
293     private void cbLinefeedItemStateChanged(ItemEvent evt) {//GEN-FIRST:event_cbLinefeedItemStateChanged
294 		if (ItemEvent.SELECTED == evt.getStateChange()) {
295 			cbLinefeed.setToolTipText(Objects.toString(cbLinefeed.getSelectedItem(), null));
296 
297 			fireChangedListener();
298 		}
299     }//GEN-LAST:event_cbLinefeedItemStateChanged
300 
301     private void formatterLocFieldFocusLost(FocusEvent evt) {//GEN-FIRST:event_formatterLocFieldFocusLost
302 		loadEclipseFormatterFileForPreview(formatterLocField.getText(), getSelectedProfile());
303 
304 		if (!valid()) {
305 			fireChangedListener();
306 		}
307     }//GEN-LAST:event_formatterLocFieldFocusLost
308 
309     // Variables declaration - do not modify//GEN-BEGIN:variables
310     private JButton browseButton;
311     private JComboBox<String> cbLinefeed;
312     private JComboBox<String> cbProfile;
313     private JCheckBox cbUseProjectPref;
314     private JLabel errorLabel;
315     private JTextField formatterLocField;
316     private JLabel jLabel2;
317     private JLabel lblFormatterFile;
318     private JLabel lblLinefeed;
319     private JLabel lblProfile;
320     // End of variables declaration//GEN-END:variables
321 
322 		private String getLinefeed() {
323 			if (0 == cbLinefeed.getSelectedIndex()) {
324 				return "";
325 			}
326 			return cbLinefeed.getSelectedItem().toString();
327 		}
328 
329 		/**
330 		 * Returns the selected Eclipse formatter profile.
331 		 *
332 		 * @return the selected Eclipse formatter profile
333 		 */
334 		private String getSelectedProfile() {
335 			if (null != cbProfile.getSelectedItem()) {
336 				return cbProfile.getSelectedItem().toString();
337 			} else {
338 				return "";
339 			}
340 		}
341 
342 		/**
343 		 * {@inheritDoc}
344 		 */
345 		@Override
346 		public void load(Preferences preferences) {
347 			String eclipseFormatterLocation = preferences.get(EclipseJavascriptFormatterSettings.CONFIG_FILE_LOCATION, "");
348 			String eclipseFormatterProfile = preferences.get(EclipseJavascriptFormatterSettings.ACTIVE_PROFILE, "");
349 			boolean useProjectPrefs = preferences.getBoolean(EclipseJavascriptFormatterSettings.USE_PROJECT_PREFS, true);
350 			String eclipseLineFeed = preferences.get(EclipseJavascriptFormatterSettings.LINEFEED, "");
351 
352 			loadEclipseFormatterFileForPreview(eclipseFormatterLocation, eclipseFormatterProfile);
353 
354 			cbUseProjectPref.setSelected(useProjectPrefs);
355 
356 			if (StringUtils.isBlank(eclipseLineFeed)) {
357 				//default = system-dependend LF
358 				cbLinefeed.setSelectedIndex(0);
359 			} else {
360 				cbLinefeed.setSelectedItem(eclipseLineFeed);
361 			}
362 
363 			cbLinefeed.setToolTipText(Objects.toString(cbLinefeed.getSelectedItem(), null));
364 		}
365 
366 		private void loadEclipseFormatterFileForPreview(String formatterFile, String activeProfile) {
367 			String filePath;
368 
369 			try {
370 				URL url = new URL(formatterFile);
371 
372 				filePath = url.toString();
373 			} catch (IOException ex) {
374 				Path formatterFilePath = Paths.get(formatterFile);
375 				if (!formatterFilePath.isAbsolute() && project != null) {
376 					formatterFilePath = Paths.get(project.getProjectDirectory().getPath()).resolve(formatterFilePath);
377 				}
378 
379 				filePath = formatterFilePath.toAbsolutePath().toString();
380 			}
381 
382 			formatterLocField.setText(formatterFile);
383 
384 			cbProfile.setEnabled(false);
385 
386 			cbProfile.removeAllItems();
387 
388 			try {
389 				//only xml configurations contain profiles
390 				if (EclipseJavascriptFormatterSettings.isXMLConfigurationFile(filePath)) {
391 					List<String> profileNames = ConfigReader.getProfileNames(ConfigReader.readContentFromFilePath(filePath));
392 					cbProfile.addItem(NbBundle.getMessage(EclipseJavascriptFormatterOptionsPanel.class, "EclipseJavascriptFormatterOptionsPanel.chooseProfile"));
393 
394 					String entryToSelect = null;
395 					for (String profileName : profileNames) {
396 						cbProfile.addItem(profileName);
397 						if (activeProfile != null && activeProfile.equals(profileName)) {
398 							entryToSelect = profileName;
399 						}
400 					}
401 					selectProfileOrFallback(entryToSelect, profileNames);
402 					cbProfile.setEnabled(true);
403 				}
404 
405 				formatterLocField.setToolTipText(filePath);
406 			} catch (IOException | SAXException | ConfigReadException ex) {
407 				log.log(Level.INFO, "Could not parse formatter config", ex);
408 			}
409 		}
410 
411 		private void selectProfileOrFallback(String entryToSelect, List<String> profiles) {
412 			if (null != entryToSelect) {
413 				cbProfile.setSelectedItem(entryToSelect);
414 			} else if (profiles.size() == 1) {
415 				//only one entry (excl. default) -> choose the only valid item
416 				cbProfile.setSelectedIndex(1);
417 			} else {
418 				//fallback: ===choose profile==
419 				cbProfile.setSelectedIndex(0);
420 			}
421 
422 			cbProfile.setToolTipText(Objects.toString(cbProfile.getSelectedItem(), null));
423 		}
424 
425 		/**
426 		 * {@inheritDoc}
427 		 */
428 		@Override
429 		public void store(Preferences preferences) {
430 			preferences.put(EclipseJavascriptFormatterSettings.CONFIG_FILE_LOCATION, formatterLocField.getText());
431 			preferences.put(EclipseJavascriptFormatterSettings.ACTIVE_PROFILE, getSelectedProfile());
432 			preferences.putBoolean(EclipseJavascriptFormatterSettings.USE_PROJECT_PREFS, cbUseProjectPref.isSelected());
433 			preferences.put(EclipseJavascriptFormatterSettings.LINEFEED, getLinefeed());
434 		}
435 
436 		/**
437 		 * {@inheritDoc}
438 		 */
439 		@Override
440 		public boolean valid() {
441 			errorLabel.setText("");
442 
443 			String fileName = formatterLocField.getText();
444 			if (StringUtils.isBlank(fileName) && cbUseProjectPref.isSelected()) {
445 				// use configuration from .settings
446 				return true;
447 			}
448 
449 			boolean fileExists;
450 			try {
451 				new URL(fileName);
452 
453 				fileExists = true;
454 			} catch (IOException ex) {
455 				fileExists = new File(fileName).exists();
456 			}
457 
458 			boolean isXML = EclipseJavascriptFormatterSettings.isXMLConfigurationFile(fileName);
459 			boolean isEPF = EclipseJavascriptFormatterSettings.isWorkspaceMechanicFile(fileName);
460 			boolean isProjectSetting = EclipseJavascriptFormatterSettings.isProjectSetting(fileName);
461 
462 			if (!fileExists || (!isXML && !isEPF && !isProjectSetting) || cbProfile.getSelectedIndex() < 0) {
463 				errorLabel.setText("Invalid file. Please enter a valid configuration file.");
464 				return false;
465 			} else {
466 				return !isXML || cbProfile.getSelectedIndex() != 0;
467 			}
468 		}
469 	}