| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.netbeans.modules.editor.macros; |
| |
| import java.awt.AWTEvent; |
| import java.awt.EventQueue; |
| import java.awt.Toolkit; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.KeyEvent; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.io.IOException; |
| import java.lang.reflect.Method; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.MissingResourceException; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.Action; |
| import javax.swing.KeyStroke; |
| import javax.swing.border.EmptyBorder; |
| import javax.swing.event.AncestorEvent; |
| import javax.swing.event.AncestorListener; |
| import javax.swing.event.UndoableEditEvent; |
| import javax.swing.event.UndoableEditListener; |
| import javax.swing.text.AbstractDocument; |
| import javax.swing.text.DefaultEditorKit; |
| import javax.swing.text.Document; |
| import javax.swing.text.JTextComponent; |
| import javax.swing.undo.UndoableEdit; |
| import org.netbeans.api.editor.mimelookup.MimePath; |
| import org.netbeans.api.editor.settings.MultiKeyBinding; |
| import org.netbeans.core.options.keymap.api.KeyStrokeUtils; |
| import org.netbeans.editor.BaseAction; |
| import org.netbeans.editor.BaseDocument; |
| import org.netbeans.editor.BaseKit; |
| import org.netbeans.editor.Utilities; |
| import org.netbeans.modules.editor.NbEditorUtilities; |
| import org.netbeans.modules.editor.macros.storage.MacroDescription; |
| import org.netbeans.modules.editor.macros.storage.MacrosStorage; |
| import org.netbeans.modules.editor.macros.storage.ui.MacrosPanel; |
| import org.netbeans.modules.editor.settings.storage.api.EditorSettingsStorage; |
| import org.netbeans.spi.options.OptionsPanelController; |
| import org.openide.DialogDescriptor; |
| import org.openide.DialogDisplayer; |
| import org.openide.NotifyDescriptor; |
| import org.openide.text.CloneableEditorSupport; |
| import org.openide.util.Lookup; |
| import org.openide.util.NbBundle; |
| |
| /** |
| * |
| * @author Petr Nejedly |
| */ |
| public final class MacroDialogSupport { |
| |
| private static final Logger LOG = Logger.getLogger(MacroDialogSupport.class.getName()); |
| |
| private MacroDialogSupport() { |
| // no-op |
| } |
| |
| public static MacroDescription findMacro(MimePath mimeType, KeyStroke... shortcut) { |
| EditorSettingsStorage<String, MacroDescription> ess = EditorSettingsStorage.<String, MacroDescription>get(MacrosStorage.ID); |
| |
| MacroDescription macro = null; |
| |
| // try 'mimeType' specific macros |
| try { |
| Map<String, MacroDescription> macros = ess.load(mimeType, null, false); |
| macro = findByShortcut(macros, shortcut); |
| } catch (IOException ioe) { |
| LOG.log(Level.WARNING, null, ioe); |
| } |
| |
| if (macro == null) { |
| // try 'all languages' macros |
| try { |
| Map<String, MacroDescription> macros = ess.load(MimePath.EMPTY, null, false); |
| macro = findByShortcut(macros, shortcut); |
| } catch (IOException ioe) { |
| LOG.log(Level.WARNING, null, ioe); |
| } |
| } |
| |
| return macro; |
| } |
| |
| private static final MacroDescription findByShortcut(Map<String, MacroDescription> macros, KeyStroke... shortcut) { |
| for(MacroDescription m : macros.values()) { |
| outer: for(MultiKeyBinding mkb : m.getShortcuts()) { |
| if (mkb == null) { |
| // erroneous shortcut |
| LOG.warning("Null shortcut in macro definition: " + m); |
| continue; |
| } |
| if (mkb.getKeyStrokeCount() == shortcut.length) { |
| for(int i = 0; i < shortcut.length; i++) { |
| if (!mkb.getKeyStroke(i).equals(shortcut[i])) { |
| continue outer; |
| } |
| } |
| return m; |
| } |
| } |
| } |
| return null; |
| } |
| |
| public static class StartMacroRecordingAction extends BaseAction { |
| |
| static final long serialVersionUID = 1L; |
| |
| public StartMacroRecordingAction() { |
| super(BaseKit.startMacroRecordingAction, NO_RECORDING); |
| putValue(BaseAction.ICON_RESOURCE_PROPERTY, |
| "org/netbeans/modules/editor/macros/start_macro_recording.png"); // NOI18N |
| } |
| |
| public void actionPerformed(ActionEvent evt, JTextComponent target) { |
| if (target != null) { |
| if (!startRecording(target)) { |
| target.getToolkit().beep(); |
| } |
| } |
| } |
| |
| private boolean startRecording(JTextComponent c) { |
| try { |
| Method m = BaseAction.class.getDeclaredMethod("startRecording", JTextComponent.class); //NOI18N |
| m.setAccessible(true); |
| return (Boolean) m.invoke(this, c); |
| } catch (Exception e) { |
| LOG.log(Level.WARNING, "Can't call BaseAction.startRecording", e); //NOI18N |
| return false; |
| } |
| } |
| } // End of StartMacroRecordingAction class |
| |
| public static final class StopMacroRecordingAction extends BaseAction { |
| |
| static final long serialVersionUID = 1L; |
| |
| public StopMacroRecordingAction() { |
| super(BaseKit.stopMacroRecordingAction, NO_RECORDING); |
| putValue(BaseAction.ICON_RESOURCE_PROPERTY, |
| "org/netbeans/modules/editor/macros/stop_macro_recording.png"); // NOI18N |
| } |
| |
| public void actionPerformed(ActionEvent evt, JTextComponent target) { |
| if (target != null) { |
| final String macro = stopRecording(target); |
| if (macro == null) { // not recording |
| target.getToolkit().beep(); |
| } else { |
| // popup a macro dialog |
| final MacrosPanel panel = new MacrosPanel(Lookup.getDefault()); |
| panel.setBorder(new EmptyBorder(10, 10, 10, 10)); |
| panel.addAncestorListener(new AncestorListener() { |
| public void ancestorAdded(AncestorEvent event) { |
| panel.forceAddMacro(macro); |
| } |
| |
| public void ancestorRemoved(AncestorEvent event) { |
| } |
| |
| public void ancestorMoved(AncestorEvent event) { |
| } |
| }); |
| panel.getModel().load(); |
| |
| final DialogDescriptor descriptor = new DialogDescriptor( |
| panel, |
| NbBundle.getMessage(MacroDialogSupport.class, "Macros_Dialog_title"), //NOI18N |
| true, |
| new Object[] { |
| DialogDescriptor.OK_OPTION, |
| DialogDescriptor.CANCEL_OPTION |
| }, |
| DialogDescriptor.OK_OPTION, |
| DialogDescriptor.DEFAULT_ALIGN, |
| null, |
| null |
| ); |
| descriptor.setClosingOptions (new Object[] { |
| DialogDescriptor.OK_OPTION, |
| DialogDescriptor.CANCEL_OPTION |
| }); |
| descriptor.setValid(false); |
| panel.getModel().addPropertyChangeListener(new PropertyChangeListener() { |
| public void propertyChange(PropertyChangeEvent evt) { |
| if (evt.getPropertyName() == null || OptionsPanelController.PROP_CHANGED.equals(evt.getPropertyName())) { |
| descriptor.setValid(panel.getModel().isChanged()); |
| } |
| } |
| }); |
| |
| DialogDisplayer.getDefault ().notify (descriptor); |
| if (descriptor.getValue () == DialogDescriptor.OK_OPTION) { |
| panel.save(); |
| } |
| } |
| } |
| } |
| |
| private String stopRecording(JTextComponent c) { |
| try { |
| Method m = BaseAction.class.getDeclaredMethod("stopRecording", JTextComponent.class); //NOI18N |
| m.setAccessible(true); |
| return (String) m.invoke(this, c); |
| } catch (Exception e) { |
| LOG.log(Level.WARNING, "Can't call BaseAction.stopRecording", e); //NOI18N |
| return null; |
| } |
| } |
| } // End of StopMacroRecordingAction class |
| |
| public static class RunMacroAction extends BaseAction { |
| |
| static final long serialVersionUID = 1L; |
| static HashSet<String> runningActions = new HashSet<String>(); |
| |
| public static final String runMacroAction = "run-macro"; //NOI18N |
| |
| public RunMacroAction() { |
| super(runMacroAction, NO_RECORDING); //NOI18N |
| } |
| |
| protected void error(JTextComponent target, String messageKey, Object... params) { |
| String message; |
| try { |
| message = NbBundle.getMessage(RunMacroAction.class, messageKey, params); |
| } catch (MissingResourceException e) { |
| message = "Error in macro: " + messageKey; //NOI18N |
| } |
| |
| NotifyDescriptor descriptor = new NotifyDescriptor.Message(message, NotifyDescriptor.ERROR_MESSAGE); // NOI18N |
| |
| Toolkit.getDefaultToolkit().beep(); |
| DialogDisplayer.getDefault().notify(descriptor); |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.log(Level.FINE, null, new Throwable(message)); |
| } |
| } |
| |
| public void actionPerformed(ActionEvent evt, JTextComponent target) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("actionCommand='" + evt.getActionCommand() //NOI18N |
| + "', modifiers=" + evt.getModifiers() //NOI18N |
| + ", when=" + evt.getWhen() //NOI18N |
| + ", paramString='" + evt.paramString() + "'"); //NOI18N |
| } |
| |
| if (target == null) { |
| return; |
| } |
| |
| BaseKit kit = Utilities.getKit(target); |
| if (kit == null) { |
| return; |
| } |
| |
| BaseDocument doc = Utilities.getDocument(target); |
| if (doc == null) { |
| return; |
| } |
| |
| // changed as reponse to #250157: other events may get fired during |
| // the course of key binding processing and if an event is processed |
| // as nested (i.e. hierarchy change resulting from a component retracting from the screen), |
| // thie following test would fail. |
| AWTEvent maybeKeyEvent = EventQueue.getCurrentEvent(); |
| KeyStroke keyStroke = null; |
| |
| if (maybeKeyEvent instanceof KeyEvent) { |
| keyStroke = KeyStroke.getKeyStrokeForEvent((KeyEvent) maybeKeyEvent); |
| } |
| |
| // try simple keystorkes first |
| MimePath mimeType = MimePath.parse(NbEditorUtilities.getMimeType(target)); |
| MacroDescription macro = null; |
| if (keyStroke != null) { |
| macro = findMacro(mimeType, keyStroke); |
| } else { |
| LOG.warning("KeyStroke could not be created for event " + maybeKeyEvent); |
| } |
| if (macro == null) { |
| // if not found, try action command, which should contain complete multi keystroke |
| KeyStroke[] shortcut = KeyStrokeUtils.getKeyStrokes(evt.getActionCommand()); |
| if (shortcut != null) { |
| macro = findMacro(mimeType, shortcut); |
| } else { |
| LOG.warning("KeyStroke could not be created for action command " + evt.getActionCommand()); |
| } |
| } |
| |
| if (macro == null) { |
| error(target, "macro-not-found", KeyStrokeUtils.getKeyStrokeAsText(keyStroke)); // NOI18N |
| return; |
| } |
| |
| if (!runningActions.add(macro.getName())) { // this macro is already running, beware of loops |
| error(target, "macro-loop", macro.getName()); // NOI18N |
| return; |
| } |
| try { |
| runMacro(target, doc, kit, macro); |
| } finally { |
| runningActions.remove(macro.getName()); |
| } |
| } |
| |
| private void runMacro(JTextComponent component, BaseDocument doc, BaseKit kit, MacroDescription macro) { |
| StringBuilder actionName = new StringBuilder(); |
| char[] command = macro.getCode().toCharArray(); |
| int len = command.length; |
| |
| sendUndoableEdit(doc, CloneableEditorSupport.BEGIN_COMMIT_GROUP); |
| try { |
| for (int i = 0; i < len; i++) { |
| if (Character.isWhitespace(command[i])) { |
| continue; |
| } |
| if (command[i] == '"') { //NOI18N |
| while (++i < len && command[i] != '"') { //NOI18N |
| char ch = command[i]; |
| if (ch == '\\') { //NOI18N |
| if (++i >= len) { // '\' at the end |
| error(component, "macro-malformed", macro.getName()); // NOI18N |
| return; |
| } |
| ch = command[i]; |
| if (ch != '"' && ch != '\\') { // neither \\ nor \" // NOI18N |
| error(component, "macro-malformed", macro.getName()); // NOI18N |
| return; |
| } // else fall through |
| } |
| Action a = component.getKeymap().getDefaultAction(); |
| |
| if (a != null) { |
| ActionEvent newEvt = new ActionEvent(component, 0, new String(new char[]{ch})); |
| if (a instanceof BaseAction) { |
| ((BaseAction) a).updateComponent(component); |
| ((BaseAction) a).actionPerformed(newEvt, component); |
| } else { |
| a.actionPerformed(newEvt); |
| } |
| } |
| } |
| } else { // parse the action name |
| actionName.setLength(0); |
| while (i < len && !Character.isWhitespace(command[i])) { |
| char ch = command[i++]; |
| if (ch == '\\') { //NOI18N |
| if (i >= len) { // macro ending with single '\' |
| error(component, "macro-malformed", macro.getName()); // NOI18N |
| return; |
| } |
| ch = command[i++]; |
| if (ch != '\\' && !Character.isWhitespace(ch)) { //NOI18N |
| error(component, "macro-malformed", macro.getName()); // neither "\\" nor "\ " // NOI18N |
| return; |
| } // else fall through |
| } |
| actionName.append(ch); |
| } |
| // execute the action |
| Action a = kit.getActionByName(actionName.toString()); |
| if (a != null) { |
| ActionEvent fakeEvt = new ActionEvent(component, 0, ""); //NOI18N |
| if (a instanceof BaseAction) { |
| ((BaseAction) a).updateComponent(component); |
| ((BaseAction) a).actionPerformed(fakeEvt, component); |
| } else { |
| a.actionPerformed(fakeEvt); |
| } |
| if (DefaultEditorKit.insertBreakAction.equals(actionName.toString())) { |
| Action def = component.getKeymap().getDefaultAction(); |
| ActionEvent fakeEvt10 = new ActionEvent(component, 0, new String(new byte[]{10})); |
| if (def instanceof BaseAction) { |
| ((BaseAction) def).updateComponent(component); |
| ((BaseAction) def).actionPerformed(fakeEvt10, component); |
| } else { |
| def.actionPerformed(fakeEvt10); |
| } |
| } |
| } else { |
| error(component, "macro-unknown-action", macro.getName(), actionName.toString()); // NOI18N |
| return; |
| } |
| } |
| } |
| } finally { |
| sendUndoableEdit(doc, CloneableEditorSupport.END_COMMIT_GROUP); |
| } |
| } |
| } // End of RunMacroAction class |
| |
| |
| private static void sendUndoableEdit(Document d, UndoableEdit ue) { |
| if(d instanceof AbstractDocument) { |
| UndoableEditListener[] uels = ((AbstractDocument)d).getUndoableEditListeners(); |
| UndoableEditEvent ev = new UndoableEditEvent(d, ue); |
| for(UndoableEditListener uel : uels) { |
| uel.undoableEditHappened(ev); |
| } |
| } |
| } |
| } |