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    * markiewb - initial API and implementation and/or initial documentation
9    * bahlef
10   */
11  package de.funfried.netbeans.plugins.external.formatter.ui.options;
12  
13  import java.awt.BorderLayout;
14  import java.awt.Component;
15  import java.awt.Container;
16  import java.awt.Cursor;
17  import java.awt.Font;
18  import java.awt.Frame;
19  import java.awt.event.ActionEvent;
20  import java.awt.event.ActionListener;
21  import java.awt.event.ItemEvent;
22  import java.awt.event.ItemListener;
23  import java.awt.event.MouseAdapter;
24  import java.awt.event.MouseEvent;
25  import java.net.MalformedURLException;
26  import java.net.URL;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Objects;
33  import java.util.logging.Level;
34  import java.util.logging.Logger;
35  import java.util.prefs.BackingStoreException;
36  import java.util.prefs.Preferences;
37  
38  import javax.swing.BorderFactory;
39  import javax.swing.DefaultListCellRenderer;
40  import javax.swing.GroupLayout;
41  import javax.swing.JCheckBox;
42  import javax.swing.JComboBox;
43  import javax.swing.JLabel;
44  import javax.swing.JList;
45  import javax.swing.JPanel;
46  import javax.swing.JSpinner;
47  import javax.swing.LayoutStyle;
48  import javax.swing.SpinnerNumberModel;
49  import javax.swing.SwingConstants;
50  import javax.swing.event.ChangeEvent;
51  import javax.swing.event.ChangeListener;
52  
53  import org.apache.commons.lang3.StringUtils;
54  import org.netbeans.api.java.lexer.JavaTokenId;
55  import org.netbeans.api.project.Project;
56  import org.netbeans.spi.options.OptionsPanelController.Keywords;
57  import org.openide.awt.HtmlBrowser;
58  import org.openide.awt.Mnemonics;
59  import org.openide.util.ChangeSupport;
60  import org.openide.util.Exceptions;
61  import org.openide.util.Lookup;
62  import org.openide.util.NbBundle;
63  import org.openide.util.WeakListeners;
64  
65  import de.funfried.netbeans.plugins.external.formatter.FormatterService;
66  import de.funfried.netbeans.plugins.external.formatter.MimeType;
67  import de.funfried.netbeans.plugins.external.formatter.ui.customizer.VerifiableConfigPanel;
68  
69  /**
70   * The options panel for this plugin.
71   *
72   * @author markiewb
73   * @author bahlef
74   */
75  @Keywords(location = "Editor", tabTitle = "External Formatter", keywords = { "eclipse", "google", "spring", "java", "external", "format", "formatter", "eclipse formatter", "google formatter",
76  		"spring formatter", "external formatter", "sql" })
77  public class ExternalFormatterPanel extends JPanel implements VerifiableConfigPanel, ChangeListener {
78  	/** The unique serial version ID. */
79  	private static final long serialVersionUID = 1L;
80  
81  	/** {@link Logger} of this class. */
82  	private static final Logger log = Logger.getLogger(ExternalFormatterPanel.class.getName());
83  
84  	/** Holds all {@link FormatterOptionsPanel}s with thier fromatter ID in their supported mime type. */
85  	private transient final Map<MimeType, Map<String, FormatterOptionsPanel>> formatterOptions = new HashMap<>();
86  
87  	/** Holds all {@link FormatterService}s in a {@link Map} with their supported mime type as the key. */
88  	private transient final Map<MimeType, List<FormatterService>> formatterIdsPerMimeType = new HashMap<>();
89  
90  	/** Holder of the currently active formatter ID per mimeType. */
91  	private transient final Map<MimeType, String> activeFormatterId = new HashMap<>();
92  
93  	/** {@link ChangeSupport} to notify about changed preference components. */
94  	private transient final ChangeSupport changeSupport;
95  
96  	/** The {@link Preferences} modified by this options dialog. */
97  	private transient final Preferences preferences;
98  
99  	/** Internal helper flag which is set to {@code true} while switching the mime type, so there won't be a change listener event for that. */
100 	private transient volatile boolean switchingMimeType = false;
101 
102 	/** The {@link Project} which this panel is used to change the settings for or {@code null} if this panel is used to change the global settings. */
103 	private transient final Project project;
104 
105 	/** Internal flag which defines whether or not the selection of another formatter should be handled or not. */
106 	private transient boolean formatterSelectionActive = true;
107 
108 	/**
109 	 * Creates a new instance of {@link ExternalFormatterPanel}.
110 	 *
111 	 * @param preferences the {@link Preferences}
112 	 * @param project the {@link Project} which this panel is used to change the
113 	 *        settings for or {@code null} if this panel is used to
114 	 *        change the global settings
115 	 */
116 	public ExternalFormatterPanel(Preferences preferences, Project project) {
117 		this.preferences = preferences;
118 		this.project = project;
119 
120 		this.changeSupport = new ChangeSupport(this);
121 
122 		Collection<? extends FormatterService> formatterServices = Lookup.getDefault().lookupAll(FormatterService.class);
123 		for (FormatterService formatterService : formatterServices) {
124 			List<MimeType> mimeTypes = formatterService.getSupportedMimeTypes();
125 			for (MimeType mimeType : mimeTypes) {
126 				List<FormatterService> formatterServicesOfMimeType = formatterIdsPerMimeType.get(mimeType);
127 				if (formatterServicesOfMimeType == null) {
128 					formatterServicesOfMimeType = new ArrayList<>();
129 				}
130 
131 				formatterServicesOfMimeType.add(formatterService);
132 
133 				formatterIdsPerMimeType.put(mimeType, formatterServicesOfMimeType);
134 			}
135 		}
136 
137 		initComponents();
138 		initMimeTypes();
139 
140 		updateEnabledState();
141 	}
142 
143 	/**
144 	 * Adds a given {@link ChangeListener} to this option dialog, which will be notified as
145 	 * soon as an user has changed the state of the components inside this options dialog.
146 	 *
147 	 * @param listener the {@link ChangeListener} to add
148 	 */
149 	public void addChangeListener(ChangeListener listener) {
150 		changeSupport.addChangeListener(listener);
151 	}
152 
153 	/**
154 	 * Removes a given {@link ChangeListener} from this option dialog.
155 	 *
156 	 * @param listener the {@link ChangeListener} which should be removed
157 	 */
158 	public void removeChangeListener(ChangeListener listener) {
159 		changeSupport.removeChangeListener(listener);
160 	}
161 
162 	/**
163 	 * Fires a change event to all registered {@link ChangeListener}s.
164 	 */
165 	private void fireChangedListener() {
166 		changeSupport.fireChange();
167 	}
168 
169 	/**
170 	 * Initializes the mime type items of the {@link #chooseMimeTypeCmbBox}
171 	 * {@link JComboBox}.
172 	 */
173 	private void initMimeTypes() {
174 		chooseMimeTypeCmbBox.removeAllItems();
175 
176 		for (MimeType mimeType : MimeType.values()) {
177 			chooseMimeTypeCmbBox.addItem(new ExtValue(mimeType.toString(), mimeType.getDisplayName()));
178 		}
179 
180 		chooseMimeTypeCmbBox.setSelectedIndex(0);
181 		chooseMimeTypeCmbBox.setToolTipText(Objects.toString(chooseMimeTypeCmbBox.getSelectedItem(), null));
182 	}
183 
184 	/**
185 	 * Sets the currently active formatter for the given {@code mimeType}.
186 	 *
187 	 * @param mimeType the mime type
188 	 * @param formatterId the formatter service ID
189 	 * @param fireChange {@code true} to fire the change listener
190 	 */
191 	private void setActiveFormatter(MimeType mimeType, String formatterId, boolean fireChange) {
192 		activeFormatterId.put(mimeType, formatterId);
193 
194 		if (fireChange) {
195 			fireChangedListener();
196 		}
197 	}
198 
199 	/**
200 	 * Returns the {@link FormatterOptionsPanel} for the given {@code mimeType} and {@code formatterId},
201 	 * if this {@link FormatterOptionsPanel} is requested for the first time the preferences will also be
202 	 * loaded.
203 	 *
204 	 * @param mimeType the mime type
205 	 * @param formatterId the formatter service ID
206 	 *
207 	 * @return the {@link FormatterOptionsPanel} for the given {@code mimeType} and {@code formatterId},
208 	 *         or {@code null} if it cannot be found
209 	 */
210 	private synchronized FormatterOptionsPanel getFormatterOptionsPanel(MimeType mimeType, String formatterId) {
211 		Map<String, FormatterOptionsPanel> formatterOptionPanels = formatterOptions.get(mimeType);
212 		if (formatterOptionPanels == null) {
213 			formatterOptionPanels = new HashMap<>();
214 
215 			formatterOptions.put(mimeType, formatterOptionPanels);
216 		}
217 
218 		FormatterOptionsPanel optionsPanel = formatterOptionPanels.get(formatterId);
219 		if (optionsPanel == null) {
220 			List<FormatterService> formatterServices = formatterIdsPerMimeType.get(mimeType);
221 			if (formatterServices != null) {
222 				for (FormatterService formatterService : formatterServices) {
223 					if (formatterService != null && Objects.equals(formatterId, formatterService.getId())) {
224 						optionsPanel = formatterService.createOptionsPanel(project);
225 						optionsPanel.load(preferences);
226 
227 						formatterOptionPanels.put(formatterId, optionsPanel);
228 
229 						formatterOptions.put(mimeType, formatterOptionPanels);
230 					}
231 				}
232 			}
233 		}
234 
235 		return optionsPanel;
236 	}
237 
238 	/**
239 	 * This method is called from within the constructor to initialize the form.
240 	 * WARNING: Do NOT modify this code. The content of this method is always regenerated by the Form Editor.
241 	 */
242     // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
243     private void initComponents() {
244 
245         txtProjectSpecificHint = new JLabel();
246         cbShowNotifications = new JCheckBox();
247         btnDonate = new JLabel();
248         btnVisitHomePage = new JLabel();
249         useIndentationSettingsChkBox = new JCheckBox();
250         overrideTabSizeSpn = new JSpinner();
251         overrideTabSizeChkBox = new JCheckBox();
252         formatterOptionsPanel = new JPanel();
253         chooseLanguageLbl = new JLabel();
254         chooseMimeTypeCmbBox = new JComboBox<>();
255         useFormatterLbl = new JLabel();
256         chooseFormatterCmbBox = new JComboBox<>();
257 
258         Mnemonics.setLocalizedText(txtProjectSpecificHint, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.txtProjectSpecificHint.text")); // NOI18N
259 
260         Mnemonics.setLocalizedText(cbShowNotifications, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.cbShowNotifications.text")); // NOI18N
261         cbShowNotifications.setToolTipText(NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.cbShowNotifications.toolTipText")); // NOI18N
262         cbShowNotifications.addActionListener(new ActionListener() {
263             public void actionPerformed(ActionEvent evt) {
264                 cbShowNotificationsActionPerformed(evt);
265             }
266         });
267 
268         btnDonate.setHorizontalAlignment(SwingConstants.RIGHT);
269         Mnemonics.setLocalizedText(btnDonate, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.btnDonate.text")); // NOI18N
270         btnDonate.setToolTipText(NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.btnDonate.toolTipText")); // NOI18N
271         btnDonate.setCursor(new Cursor(Cursor.HAND_CURSOR));
272         btnDonate.addMouseListener(new MouseAdapter() {
273             public void mouseClicked(MouseEvent evt) {
274                 btnDonateMouseClicked(evt);
275             }
276         });
277 
278         btnVisitHomePage.setHorizontalAlignment(SwingConstants.RIGHT);
279         Mnemonics.setLocalizedText(btnVisitHomePage, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.btnVisitHomePage.text")); // NOI18N
280         btnVisitHomePage.setToolTipText(NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.btnVisitHomePage.toolTipText")); // NOI18N
281         btnVisitHomePage.setCursor(new Cursor(Cursor.HAND_CURSOR));
282         btnVisitHomePage.addMouseListener(new MouseAdapter() {
283             public void mouseClicked(MouseEvent evt) {
284                 btnVisitHomePageMouseClicked(evt);
285             }
286         });
287 
288         useIndentationSettingsChkBox.setSelected(true);
289         Mnemonics.setLocalizedText(useIndentationSettingsChkBox, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.useIndentationSettingsChkBox.text")); // NOI18N
290         useIndentationSettingsChkBox.setToolTipText(NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.useIndentationSettingsChkBox.toolTipText")); // NOI18N
291         useIndentationSettingsChkBox.addActionListener(new ActionListener() {
292             public void actionPerformed(ActionEvent evt) {
293                 useIndentationSettingsChkBoxActionPerformed(evt);
294             }
295         });
296 
297         overrideTabSizeSpn.setModel(new SpinnerNumberModel(4, 1, 20, 1));
298         overrideTabSizeSpn.setToolTipText(NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.overrideTabSizeSpn.toolTipText")); // NOI18N
299         overrideTabSizeSpn.setEnabled(false);
300         overrideTabSizeSpn.addChangeListener(new ChangeListener() {
301             public void stateChanged(ChangeEvent evt) {
302                 overrideTabSizeSpnStateChanged(evt);
303             }
304         });
305 
306         Mnemonics.setLocalizedText(overrideTabSizeChkBox, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.overrideTabSizeChkBox.text")); // NOI18N
307         overrideTabSizeChkBox.setToolTipText(NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.overrideTabSizeChkBox.toolTipText")); // NOI18N
308         overrideTabSizeChkBox.addActionListener(new ActionListener() {
309             public void actionPerformed(ActionEvent evt) {
310                 overrideTabSizeChkBoxActionPerformed(evt);
311             }
312         });
313 
314         formatterOptionsPanel.setLayout(new BorderLayout());
315 
316         Mnemonics.setLocalizedText(chooseLanguageLbl, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.chooseLanguageLbl.text")); // NOI18N
317 
318         chooseMimeTypeCmbBox.setRenderer(new MimeTypesListCellRenderer());
319         chooseMimeTypeCmbBox.addItemListener(new ItemListener() {
320             public void itemStateChanged(ItemEvent evt) {
321                 chooseMimeTypeCmbBoxItemStateChanged(evt);
322             }
323         });
324 
325         Mnemonics.setLocalizedText(useFormatterLbl, NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterPanel.useFormatterLbl.text")); // NOI18N
326 
327         chooseFormatterCmbBox.addItemListener(new ItemListener() {
328             public void itemStateChanged(ItemEvent evt) {
329                 chooseFormatterCmbBoxItemStateChanged(evt);
330             }
331         });
332 
333         GroupLayout layout = new GroupLayout(this);
334         this.setLayout(layout);
335         layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
336             .addGroup(layout.createSequentialGroup()
337                 .addContainerGap()
338                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
339                     .addGroup(layout.createSequentialGroup()
340                         .addGap(0, 0, Short.MAX_VALUE)
341                         .addComponent(txtProjectSpecificHint))
342                     .addGroup(layout.createSequentialGroup()
343                         .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
344                             .addComponent(formatterOptionsPanel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
345                             .addGroup(layout.createSequentialGroup()
346                                 .addComponent(btnVisitHomePage, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
347                                 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
348                                 .addComponent(btnDonate, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
349                             .addGroup(layout.createSequentialGroup()
350                                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
351                                     .addComponent(cbShowNotifications)
352                                     .addGroup(layout.createSequentialGroup()
353                                         .addComponent(useIndentationSettingsChkBox)
354                                         .addGap(18, 18, 18)
355                                         .addComponent(overrideTabSizeChkBox)
356                                         .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
357                                         .addComponent(overrideTabSizeSpn, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
358                                     .addGroup(layout.createSequentialGroup()
359                                         .addComponent(chooseLanguageLbl)
360                                         .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
361                                         .addComponent(chooseMimeTypeCmbBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
362                                         .addGap(18, 18, 18)
363                                         .addComponent(useFormatterLbl)
364                                         .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
365                                         .addComponent(chooseFormatterCmbBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)))
366                                 .addGap(0, 0, Short.MAX_VALUE)))
367                         .addContainerGap())))
368         );
369         layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
370             .addGroup(layout.createSequentialGroup()
371                 .addComponent(txtProjectSpecificHint)
372                 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
373                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
374                     .addComponent(chooseLanguageLbl)
375                     .addComponent(chooseMimeTypeCmbBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
376                     .addComponent(useFormatterLbl)
377                     .addComponent(chooseFormatterCmbBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
378                 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
379                 .addComponent(formatterOptionsPanel, GroupLayout.DEFAULT_SIZE, 31, Short.MAX_VALUE)
380                 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
381                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
382                     .addComponent(useIndentationSettingsChkBox)
383                     .addComponent(overrideTabSizeSpn, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
384                     .addComponent(overrideTabSizeChkBox))
385                 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
386                 .addComponent(cbShowNotifications)
387                 .addGap(13, 13, 13)
388                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
389                     .addComponent(btnDonate, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
390                     .addComponent(btnVisitHomePage, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
391                 .addContainerGap())
392         );
393     }// </editor-fold>//GEN-END:initComponents
394 
395 	private void btnDonateMouseClicked(MouseEvent evt) {//GEN-FIRST:event_btnDonateMouseClicked
396 		Frame parentFrame = null;
397 
398 		Container parent = this.getParent();
399 		while(parent != null) {
400 			if(parent instanceof Frame) {
401 				parentFrame = (Frame) parent;
402 
403 				break;
404 			}
405 
406 			parent = parent.getParent();
407 		}
408 
409 		ExternalFormatterSupportDialog dialog = new ExternalFormatterSupportDialog(parentFrame, parentFrame != null);
410 		dialog.setTitle(NbBundle.getMessage(ExternalFormatterPanel.class, "ExternalFormatterSupportDialog.title.text"));
411 		dialog.pack();
412 		dialog.setLocationRelativeTo(this);
413 		dialog.setVisible(true);
414 		dialog.toFront();
415 
416 //		try {
417 //			HtmlBrowser.URLDisplayer.getDefault().showURLExternal(new URL("https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=926F5XBCTK2LQ&source=url"));
418 //		} catch (MalformedURLException ex) {
419 //			Exceptions.printStackTrace(ex);
420 //		}
421 	}//GEN-LAST:event_btnDonateMouseClicked
422 
423 	private void btnVisitHomePageMouseClicked(MouseEvent evt) {//GEN-FIRST:event_btnVisitHomePageMouseClicked
424 		try {
425 			HtmlBrowser.URLDisplayer.getDefault().showURLExternal(new URL("https://github.com/funfried/externalcodeformatter_for_netbeans/"));
426 		} catch (MalformedURLException ex) {
427 			Exceptions.printStackTrace(ex);
428 		}
429 	}//GEN-LAST:event_btnVisitHomePageMouseClicked
430 
431     private void useIndentationSettingsChkBoxActionPerformed(ActionEvent evt) {//GEN-FIRST:event_useIndentationSettingsChkBoxActionPerformed
432 		updateEnabledState();
433 		fireChangedListener();
434     }//GEN-LAST:event_useIndentationSettingsChkBoxActionPerformed
435 
436     private void chooseMimeTypeCmbBoxItemStateChanged(ItemEvent evt) {//GEN-FIRST:event_chooseMimeTypeCmbBoxItemStateChanged
437 		try {
438 			switchingMimeType = true;
439 
440 			if (ItemEvent.SELECTED == evt.getStateChange()) {
441 				chooseFormatterCmbBox.removeAllItems();
442 
443 				formatterSelectionActive = false;
444 
445 				MimeType selectedMimeType = MimeType.valueOf(getSelectedValue(chooseMimeTypeCmbBox));
446 
447 				List<FormatterService> formatterServices = formatterIdsPerMimeType.get(selectedMimeType);
448 				if (formatterServices != null) {
449 					ExtValue selected = new ExtValue(Settings.DEFAULT_FORMATTER, "Internal NetBeans formatter");
450 					chooseFormatterCmbBox.addItem(selected);
451 
452 					for (FormatterService formatterService : formatterServices) {
453 						String formatterId = formatterService.getId();
454 						ExtValue value = new ExtValue(formatterId, formatterService.getDisplayName());
455 
456 						chooseFormatterCmbBox.addItem(value);
457 
458 						if (Objects.equals(activeFormatterId.get(selectedMimeType), formatterId)) {
459 							selected = value;
460 						}
461 					}
462 
463 					formatterSelectionActive = true;
464 
465 					chooseFormatterCmbBox.setSelectedItem(selected);
466 					chooseFormatterCmbBox.setToolTipText(Objects.toString(chooseFormatterCmbBox.getSelectedItem(), null));
467 				} else {
468 					formatterSelectionActive = true;
469 				}
470 			}
471 
472 			chooseMimeTypeCmbBox.setToolTipText(Objects.toString(chooseMimeTypeCmbBox.getSelectedItem(), null));
473 		} finally {
474 			switchingMimeType = false;
475 		}
476     }//GEN-LAST:event_chooseMimeTypeCmbBoxItemStateChanged
477 
478     private void chooseFormatterCmbBoxItemStateChanged(ItemEvent evt) {//GEN-FIRST:event_chooseFormatterCmbBoxItemStateChanged
479 		if (!formatterSelectionActive) {
480 			return;
481 		}
482 
483 		if (ItemEvent.DESELECTED == evt.getStateChange()) {
484 			formatterOptionsPanel.setBorder(null);
485 			formatterOptionsPanel.removeAll();
486 		} else if (ItemEvent.SELECTED == evt.getStateChange()) {
487 			MimeType selectedMimeType = MimeType.valueOf(getSelectedValue(chooseMimeTypeCmbBox));
488 			String selectedFormatterId = getSelectedValue(chooseFormatterCmbBox);
489 
490 			FormatterOptionsPanel optionsPanel = getFormatterOptionsPanel(selectedMimeType, selectedFormatterId);
491 			if (optionsPanel != null) {
492 				formatterOptionsPanel.setBorder(BorderFactory.createEtchedBorder());
493 				formatterOptionsPanel.add(optionsPanel.getComponent(), BorderLayout.CENTER);
494 
495 				optionsPanel.addChangeListener(WeakListeners.change(this, optionsPanel));
496 			}
497 
498 			setActiveFormatter(selectedMimeType, selectedFormatterId, !switchingMimeType);
499 		}
500 
501 		chooseFormatterCmbBox.setToolTipText(Objects.toString(chooseFormatterCmbBox.getSelectedItem(), null));
502     }//GEN-LAST:event_chooseFormatterCmbBoxItemStateChanged
503 
504     private void overrideTabSizeChkBoxActionPerformed(ActionEvent evt) {//GEN-FIRST:event_overrideTabSizeChkBoxActionPerformed
505 		updateEnabledState();
506 		fireChangedListener();
507     }//GEN-LAST:event_overrideTabSizeChkBoxActionPerformed
508 
509     private void overrideTabSizeSpnStateChanged(ChangeEvent evt) {//GEN-FIRST:event_overrideTabSizeSpnStateChanged
510 		overrideTabSizeSpn.setToolTipText(Objects.toString(overrideTabSizeSpn.getValue(), null));
511 
512 		fireChangedListener();
513     }//GEN-LAST:event_overrideTabSizeSpnStateChanged
514 
515     private void cbShowNotificationsActionPerformed(ActionEvent evt) {//GEN-FIRST:event_cbShowNotificationsActionPerformed
516 		fireChangedListener();
517     }//GEN-LAST:event_cbShowNotificationsActionPerformed
518 
519 		/**
520 		 * {@inheritDoc}
521 		 */
522 		@Override
523 		@SuppressWarnings("deprecation")
524 		public void load() {
525 			MimeType selectedMimeType = MimeType.valueOf(getSelectedValue(chooseMimeTypeCmbBox));
526 			String javaMimeType = JavaTokenId.language().mimeType();
527 
528 			for (MimeType mimeType : formatterIdsPerMimeType.keySet()) {
529 				String activeFormatter = preferences.get(Settings.ENABLED_FORMATTER_PREFIX + mimeType.toString(), Settings.DEFAULT_FORMATTER);
530 				if (Settings.DEFAULT_FORMATTER.equals(activeFormatter) && mimeType.canHandle(javaMimeType)) {
531 					activeFormatter = preferences.get(Settings.ENABLED_FORMATTER, Settings.DEFAULT_FORMATTER);
532 
533 					preferences.remove(Settings.ENABLED_FORMATTER);
534 				}
535 
536 				setActiveFormatter(mimeType, activeFormatter, false);
537 
538 				if (Objects.equals(selectedMimeType, mimeType)) {
539 					chooseFormatterCmbBox.setSelectedItem(new ExtValue(activeFormatter, null));
540 					chooseFormatterCmbBox.setToolTipText(Objects.toString(chooseFormatterCmbBox.getSelectedItem(), null));
541 				}
542 			}
543 
544 			boolean showNotifications = preferences.getBoolean(Settings.SHOW_NOTIFICATIONS, false);
545 			boolean useIndentationSettings = preferences.getBoolean(Settings.ENABLE_USE_OF_INDENTATION_SETTINGS, true);
546 			boolean overrideTabSize = preferences.getBoolean(Settings.OVERRIDE_TAB_SIZE, false);
547 			int overrideTabSizeValue = preferences.getInt(Settings.OVERRIDE_TAB_SIZE_VALUE, 4);
548 
549 			useIndentationSettingsChkBox.setSelected(useIndentationSettings);
550 			overrideTabSizeChkBox.setSelected(overrideTabSize);
551 			overrideTabSizeSpn.setValue(overrideTabSizeValue);
552 			overrideTabSizeSpn.setToolTipText(Objects.toString(overrideTabSizeSpn.getValue(), null));
553 
554 			cbShowNotifications.setSelected(showNotifications);
555 
556 			updateEnabledState();
557 		}
558 
559 		/**
560 		 * {@inheritDoc}
561 		 */
562 		@Override
563 		public void store() {
564 			for (MimeType mimeType : formatterIdsPerMimeType.keySet()) {
565 				preferences.put(Settings.ENABLED_FORMATTER_PREFIX + mimeType.toString(), activeFormatterId.get(mimeType));
566 
567 				Map<String, FormatterOptionsPanel> options = formatterOptions.get(mimeType);
568 				if (options != null) {
569 					for (FormatterOptionsPanel option : options.values()) {
570 						option.store(preferences);
571 					}
572 				}
573 			}
574 
575 			preferences.putBoolean(Settings.ENABLE_USE_OF_INDENTATION_SETTINGS, useIndentationSettingsChkBox.isSelected());
576 			preferences.putBoolean(Settings.OVERRIDE_TAB_SIZE, overrideTabSizeChkBox.isSelected());
577 			preferences.putInt(Settings.OVERRIDE_TAB_SIZE_VALUE, Integer.parseInt(overrideTabSizeSpn.getValue().toString()));
578 			preferences.putBoolean(Settings.SHOW_NOTIFICATIONS, cbShowNotifications.isSelected());
579 
580 			try {
581 				preferences.flush();
582 			} catch (BackingStoreException ex) {
583 				log.log(Level.WARNING, "Could not flush formatter settings", ex);
584 			}
585 
586 			ExternalFormatterPreferencesChangeSupport editorPreferencesChangeSupport = Lookup.getDefault().lookup(ExternalFormatterPreferencesChangeSupport.class);
587 			if (editorPreferencesChangeSupport != null) {
588 				editorPreferencesChangeSupport.fireChange();
589 			} else {
590 				log.warning("Could not find ExternalFormatterPreferencesChangeSupport in lookup!");
591 			}
592 		}
593 
594 		/**
595 		 * {@inheritDoc}
596 		 */
597 		@Override
598 		public boolean valid() {
599 			for (MimeType mimeType : formatterOptions.keySet()) {
600 				String activeId = activeFormatterId.get(mimeType);
601 				if (StringUtils.isNotBlank(activeId) && !Objects.equals(Settings.DEFAULT_FORMATTER, activeId)) {
602 					FormatterOptionsPanel optionsPanel = getFormatterOptionsPanel(mimeType, activeId);
603 					if (optionsPanel != null && !optionsPanel.valid()) {
604 						return false;
605 					}
606 				}
607 			}
608 
609 			return true;
610 		}
611 
612     // Variables declaration - do not modify//GEN-BEGIN:variables
613     private JLabel btnDonate;
614     private JLabel btnVisitHomePage;
615     private JCheckBox cbShowNotifications;
616     private JComboBox<ExtValue> chooseFormatterCmbBox;
617     private JLabel chooseLanguageLbl;
618     private JComboBox<ExtValue> chooseMimeTypeCmbBox;
619     private JPanel formatterOptionsPanel;
620     private JCheckBox overrideTabSizeChkBox;
621     private JSpinner overrideTabSizeSpn;
622     private JLabel txtProjectSpecificHint;
623     private JLabel useFormatterLbl;
624     private JCheckBox useIndentationSettingsChkBox;
625     // End of variables declaration//GEN-END:variables
626 
627 		/**
628 		 * Updates the enabled state of all components regarding the lastest selections made by the user.
629 		 */
630 		private void updateEnabledState() {
631 			overrideTabSizeChkBox.setEnabled(useIndentationSettingsChkBox.isSelected());
632 			overrideTabSizeSpn.setEnabled(overrideTabSizeChkBox.isEnabled() && overrideTabSizeChkBox.isSelected());
633 
634 			txtProjectSpecificHint.setVisible(project == null);
635 		}
636 
637 		/**
638 		 * Returns the selected item of a given {@link JComboBox}. If the selected item is an instance of {@link ExtValue} the {@link ExtValue#getValue()} will be returned.
639 		 *
640 		 * @param comboBox the {@link JComboBox}
641 		 *
642 		 * @return the selected item of a given {@link JComboBox}. If the selected item is an instance of {@link ExtValue} the {@link ExtValue#getValue()} will be returned
643 		 */
644 		private String getSelectedValue(JComboBox comboBox) {
645 			Object selectedItem = comboBox.getSelectedItem();
646 			if (selectedItem != null) {
647 				if (selectedItem instanceof ExtValue) {
648 					return ((ExtValue) selectedItem).getValue();
649 				} else {
650 					return selectedItem.toString();
651 				}
652 			}
653 
654 			return null;
655 		}
656 
657 		/**
658 		 * {@inheritDoc}
659 		 */
660 		@Override
661 		public void stateChanged(ChangeEvent e) {
662 			fireChangedListener();
663 		}
664 
665 		/**
666 		 * A renderer for the {@link #chooseMimeTypeCmbBox} which shows every entry in bold which has an external formatter select.
667 		 */
668 		private class MimeTypesListCellRenderer extends DefaultListCellRenderer {
669 
670 			/**
671 			 * {@inheritDoc}
672 			 */
673 			@Override
674 			public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
675 				Component comp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
676 
677 				if (value != null) {
678 					String mimeType = value.toString();
679 					if (value instanceof ExtValue) {
680 						mimeType = ((ExtValue) value).getValue();
681 					}
682 
683 					if (!Objects.equals(Settings.DEFAULT_FORMATTER, activeFormatterId.get(MimeType.valueOf(mimeType)))) {
684 						Font standardFont = comp.getFont();
685 
686 						comp.setFont(new Font(standardFont.getName(), Font.BOLD, standardFont.getSize()));
687 					}
688 				}
689 
690 				return comp;
691 			}
692 		}
693 
694 		/**
695 		 * A Java bean for separating between visual and logical values.
696 		 */
697 		private static class ExtValue {
698 
699 			/**
700 			 * Holder of the logical value.
701 			 */
702 			private final String value;
703 
704 			/**
705 			 * Holder of the visual value.
706 			 */
707 			private final String visualValue;
708 
709 			/**
710 			 * Constructor of this class.
711 			 *
712 			 * @param value logical value
713 			 * @param visualValue visual value
714 			 */
715 			public ExtValue(String value, String visualValue) {
716 				this.value = value;
717 				this.visualValue = visualValue;
718 			}
719 
720 			/**
721 			 * Returns the logical value.
722 			 *
723 			 * @return the logical value
724 			 */
725 			public String getValue() {
726 				return value;
727 			}
728 
729 			/**
730 			 * Returns the visual value.
731 			 *
732 			 * @return the visual value
733 			 *
734 			 * @see #toString()
735 			 */
736 			public String getVisualValue() {
737 				return visualValue;
738 			}
739 
740 			/**
741 			 * Returns the {@link #visualValue} if not {@code null}, otherwise the logical {@link #value}.
742 			 *
743 			 * @return the {@link #visualValue} if not {@code null}, otherwise the logical {@link #value}
744 			 */
745 			@Override
746 			public String toString() {
747 				return StringUtils.isBlank(visualValue) ? value : visualValue;
748 			}
749 
750 			/**
751 			 * Returns {@code true} if {@code obj} is also an {@link ExtValue} with the same logical value or if {@code obj} is a String with the same characters as the logical {@link #value}, otherwise
752 			 * {@code false}.
753 			 *
754 			 * @return {@code true} if {@code obj} is also an {@link ExtValue} with the same logical value or if {@code obj} is a String with the same characters as the logical {@link #value}, otherwise
755 			 *         {@code false}
756 			 */
757 			@Override
758 			public boolean equals(Object obj) {
759 				if (obj == null) {
760 					return false;
761 				} else if (obj instanceof ExtValue) {
762 					ExtValue other = (ExtValue) obj;
763 
764 					return Objects.equals(this.value, other.value);
765 				} else {
766 					return Objects.equals(this.value, obj);
767 				}
768 			}
769 
770 			/**
771 			 * {@inheritDoc}
772 			 */
773 			@Override
774 			public int hashCode() {
775 				int hash = 7;
776 				hash = 97 * hash + Objects.hashCode(this.value);
777 				return hash;
778 			}
779 		}
780 	}