| /* |
| * 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.web.inspect; |
| |
| import java.awt.Component; |
| import java.awt.EventQueue; |
| import java.awt.event.ItemEvent; |
| import java.awt.event.ItemListener; |
| import java.awt.event.KeyEvent; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.beans.PropertyChangeSupport; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.Action; |
| import javax.swing.Box; |
| import javax.swing.Icon; |
| import javax.swing.JCheckBoxMenuItem; |
| import javax.swing.JPopupMenu; |
| import javax.swing.JToggleButton; |
| import javax.swing.JToolBar; |
| import javax.swing.KeyStroke; |
| import javax.swing.LayoutStyle; |
| import javax.swing.SwingConstants; |
| import org.json.simple.JSONObject; |
| import org.json.simple.JSONValue; |
| import org.json.simple.parser.ParseException; |
| import org.netbeans.api.project.Project; |
| import org.netbeans.modules.web.browser.api.PageInspector; |
| import org.netbeans.modules.web.browser.spi.MessageDispatcher; |
| import org.netbeans.modules.web.browser.spi.MessageDispatcher.MessageListener; |
| import org.netbeans.modules.web.browser.spi.PageInspectionHandle; |
| import org.netbeans.modules.web.browser.spi.PageInspectorCustomizer; |
| import org.netbeans.modules.web.inspect.actions.Resource; |
| import org.netbeans.modules.web.inspect.ui.DomTCController; |
| import org.netbeans.modules.web.inspect.webkit.WebKitPageModel; |
| import org.netbeans.modules.web.inspect.webkit.knockout.KnockoutTCController; |
| import org.netbeans.modules.web.webkit.debugging.api.WebKitDebugging; |
| import org.openide.util.ImageUtilities; |
| import org.openide.util.Lookup; |
| import org.openide.util.NbBundle; |
| import org.openide.util.RequestProcessor; |
| import org.openide.util.lookup.ServiceProvider; |
| |
| /** |
| * Implementation of the {@code PageInspector}. |
| * |
| * @author Jan Stola |
| */ |
| @ServiceProvider(service=PageInspector.class) |
| public class PageInspectorImpl extends PageInspector { |
| /** Name of the toolbar component responsible for selection mode switching. */ |
| private static final String SELECTION_MODE_COMPONENT_NAME = "selectionModeSwitch"; // NOI18N |
| /** Request processor for this class. */ |
| static final RequestProcessor RP = new RequestProcessor(PageInspectorImpl.class.getName()); |
| /** Property change support. */ |
| private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this); |
| /** Current inspected page. */ |
| private PageModel pageModel; |
| /** Message listener for the inspected page. */ |
| private MessageListener messageListener; |
| /** Message dispatcher for the inspected page. */ |
| private MessageDispatcher messageDispatcher; |
| /** Lock guarding access to modifiable fields. */ |
| private final Object LOCK = new Object(); |
| /** Lock guarding access to {@code pageModel} field. */ |
| private final Object PAGE_MODEL_LOCK = new Object(); |
| /** Page inspector customizer for the inspected page. */ |
| private PageInspectorCustomizer pageInspectorCustomizer; |
| /** Listener for a page inspector customizer. */ |
| private PropertyChangeListener pageInspectorCustomizerListener; |
| |
| /** |
| * Creates a new {@code PageInspectorImpl}. |
| */ |
| public PageInspectorImpl() { |
| } |
| |
| /** |
| * Helper method that just casts {@code PageInspector.getDefault()} |
| * to {@code PageInspectorImpl}. |
| * |
| * @return value of {@code PageInspector.getDefault()} casted to {@code PageInspectorImpl}. |
| */ |
| public static PageInspectorImpl getDefault() { |
| return (PageInspectorImpl)PageInspector.getDefault(); |
| } |
| |
| @Override |
| public void inspectPage(Lookup pageContext) { |
| DomTCController.getDefault(); // Making sure that DOMTCController is initialized |
| KnockoutTCController.getDefault(); // Making sure that KnockoutTCController is initialized |
| synchronized (LOCK) { |
| PageModel oldModel = getPage(); |
| if (oldModel != null) { |
| oldModel.dispose(); |
| if (messageDispatcher != null) { |
| messageDispatcher.removeMessageListener(messageListener); |
| } |
| if (pageInspectorCustomizer != null) { |
| pageInspectorCustomizer.removePropertyChangeListener(pageInspectorCustomizerListener); |
| } |
| synchronized (PAGE_MODEL_LOCK) { |
| pageModel = null; |
| } |
| messageDispatcher = null; |
| messageListener = null; |
| pageInspectorCustomizer = null; |
| pageInspectorCustomizerListener = null; |
| } |
| Resource.clearCache(); |
| WebKitDebugging webKit = pageContext.lookup(WebKitDebugging.class); |
| if (webKit != null) { |
| PageModel newModel = new WebKitPageModel(pageContext); |
| messageDispatcher = pageContext.lookup(MessageDispatcher.class); |
| if (messageDispatcher != null) { |
| messageListener = new InspectionMessageListener(newModel, pageContext); |
| messageDispatcher.addMessageListener(messageListener); |
| } |
| initSelectionMode(pageContext.lookup(JToolBar.class), pageContext.lookup(JPopupMenu.class), newModel); |
| Project p = pageContext.lookup(Project.class); |
| if (p != null) { |
| pageInspectorCustomizer = p.getLookup().lookup(PageInspectorCustomizer.class); |
| if (pageInspectorCustomizer != null) { |
| pageInspectorCustomizerListener = createPageInspectorCustomizerListener(newModel, pageInspectorCustomizer); |
| pageInspectorCustomizer.addPropertyChangeListener(pageInspectorCustomizerListener); |
| newModel.setSynchronizeSelection(pageInspectorCustomizer.isHighlightSelectionEnabled()); |
| } |
| } |
| final PageInspectionHandle inspectionHandle = pageContext.lookup(PageInspectionHandle.class); |
| if (inspectionHandle != null) { |
| final PageModel model = newModel; |
| newModel.addPropertyChangeListener(new PropertyChangeListener() { |
| @Override |
| public void propertyChange(PropertyChangeEvent evt) { |
| String propertyName = evt.getPropertyName(); |
| if (PageModel.PROP_SELECTION_MODE.equals(propertyName)) { |
| inspectionHandle.setSelectionMode(model.isSelectionMode()); |
| } else if (PageModel.PROP_SYNCHRONIZE_SELECTION.equals(propertyName)) { |
| inspectionHandle.setSynchronizeSelection(model.isSynchronizeSelection()); |
| } |
| } |
| }); |
| inspectionHandle.setSelectionMode(model.isSelectionMode()); |
| inspectionHandle.setSynchronizeSelection(model.isSynchronizeSelection()); |
| } |
| synchronized (PAGE_MODEL_LOCK) { |
| pageModel = newModel; |
| } |
| } |
| } |
| firePropertyChange(PROP_MODEL, null, null); |
| } |
| |
| /** |
| * Creates a listener for a page inspector customizer changes. |
| * |
| * @param pageModel page model the customizer belongs to. |
| * @param customizer customizer to observe. |
| * @return listener for the specified page inspector customizer. |
| */ |
| PropertyChangeListener createPageInspectorCustomizerListener(final PageModel pageModel, |
| final PageInspectorCustomizer customizer) { |
| return new PropertyChangeListener() { |
| @Override |
| public void propertyChange(PropertyChangeEvent evt) { |
| String propertyName = evt.getPropertyName(); |
| if (PageInspectorCustomizer.PROPERTY_HIGHLIGHT_SELECTION.equals(propertyName)) { |
| boolean synchronize = customizer.isHighlightSelectionEnabled(); |
| pageModel.setSynchronizeSelection(synchronize); |
| } |
| } |
| }; |
| } |
| |
| /** |
| * Adds 'Selection Mode' toggle-button into the specified toolbar. |
| * |
| * @param toolBar toolbar to insert the toggle-button into (can be {@code null}). |
| * @param contextMenu popup menu to insert the toggle-button into (can be {@code null}). |
| * @param pageModel mode that the inserted toggle-button should affect. |
| */ |
| private void initSelectionMode(JToolBar toolBar, JPopupMenu contextMenu, final PageModel pageModel) { |
| if (toolBar != null) { |
| Icon selectionModeIcon = ImageUtilities.loadImageIcon("org/netbeans/modules/web/inspect/resources/selectionMode.png", true); // NOI18N |
| final JToggleButton selectionModeButton = new JToggleButton(selectionModeIcon); |
| //hardcoded shortcut for Selection Mode |
| KeyStroke ks = KeyStroke.getKeyStroke( KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK+KeyEvent.SHIFT_DOWN_MASK ); |
| selectionModeButton.putClientProperty( Action.ACCELERATOR_KEY, ks ); |
| String selectionModeTooltip = NbBundle.getMessage(PageInspectorImpl.class, "PageInspectorImpl.selectionMode"); // NOI18N |
| selectionModeButton.setToolTipText(selectionModeTooltip); |
| selectionModeButton.setName(SELECTION_MODE_COMPONENT_NAME); |
| selectionModeButton.setFocusPainted(false); |
| |
| final JCheckBoxMenuItem selectionModeMenu = new JCheckBoxMenuItem(NbBundle.getMessage(PageInspectorImpl.class, "PageInspectorImpl.selectionModeShort")); //NOI18N |
| selectionModeMenu.setAccelerator( ks ); |
| selectionModeMenu.setIcon( selectionModeIcon ); |
| ItemListener listener = new ItemListener() { |
| @Override |
| public void itemStateChanged( ItemEvent e ) { |
| final boolean selectionMode = e.getStateChange() == ItemEvent.SELECTED; |
| RP.post(new Runnable() { |
| @Override |
| public void run() { |
| pageModel.setSelectionMode(selectionMode); |
| } |
| }); |
| } |
| }; |
| selectionModeButton.addItemListener( listener ); |
| selectionModeMenu.addItemListener( listener ); |
| pageModel.addPropertyChangeListener(new PropertyChangeListener() { |
| @Override |
| public void propertyChange(PropertyChangeEvent evt) { |
| String propName = evt.getPropertyName(); |
| if (PageModel.PROP_SELECTION_MODE.equals(propName)) { |
| final boolean selectionMode = pageModel.isSelectionMode(); |
| EventQueue.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| selectionModeButton.setSelected(selectionMode); |
| selectionModeMenu.setSelected(selectionMode); |
| } |
| }); |
| } |
| } |
| }); |
| if (toolBar.getComponentCount() == 0) { |
| toolBar.addSeparator(); |
| } |
| int gapSize = LayoutStyle.getInstance().getPreferredGap( |
| selectionModeButton, selectionModeButton, |
| LayoutStyle.ComponentPlacement.RELATED, |
| SwingConstants.WEST, toolBar); |
| toolBar.add(Box.createHorizontalStrut(gapSize)); |
| toolBar.add(selectionModeButton); |
| selectionModeButton.setSelected(pageModel.isSelectionMode()); |
| selectionModeMenu.setSelected(pageModel.isSelectionMode()); |
| |
| if( null != contextMenu ) { |
| contextMenu.addSeparator(); |
| contextMenu.add( selectionModeMenu ); |
| } |
| } |
| } |
| |
| /** |
| * Uninitializes the selection mode (removes the page inspection |
| * component/s from the toolbar). |
| * |
| * @param toolBar toolBar to remove the buttons from. |
| */ |
| void uninitSelectionMode(JToolBar toolBar) { |
| if (toolBar != null) { |
| for (Component component : toolBar.getComponents()) { |
| if (SELECTION_MODE_COMPONENT_NAME.equals(component.getName())) { |
| toolBar.remove(component); |
| break; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the current inspected page. |
| * |
| * @return current inspected page. |
| */ |
| @Override |
| public PageModel getPage() { |
| synchronized (PAGE_MODEL_LOCK) { |
| return pageModel; |
| } |
| } |
| |
| /** |
| * Adds a property change listener. |
| * |
| * @param listener listener to add. |
| */ |
| @Override |
| public void addPropertyChangeListener(PropertyChangeListener listener) { |
| propChangeSupport.addPropertyChangeListener(listener); |
| } |
| |
| /** |
| * Removes a property change listener. |
| * |
| * @param listener listener to remove. |
| */ |
| @Override |
| public void removePropertyChangeListener(PropertyChangeListener listener) { |
| propChangeSupport.removePropertyChangeListener(listener); |
| } |
| |
| /** |
| * Fires the specified property change. |
| * |
| * @param propName name of the property. |
| * @param oldValue old value of the property or {@code null}. |
| * @param newValue new value of the property or {@code null}. |
| */ |
| private void firePropertyChange(String propName, Object oldValue, Object newValue) { |
| propChangeSupport.firePropertyChange(propName, oldValue, newValue); |
| } |
| |
| /** |
| * Message listener of the inspected page. |
| */ |
| private class InspectionMessageListener implements MessageListener { |
| /** Name of the message type attribute. */ |
| private static final String MESSAGE_TYPE = "message"; // NOI18N |
| /** Value of the message type attribute for the selection mode message. */ |
| private static final String MESSAGE_SELECTION_MODE = "selection_mode"; // NOI18N |
| /** Name of the attribute holding the new value of the selection mode. */ |
| private static final String MESSAGE_SELECTION_MODE_ATTR = "selectionMode"; // NOI18N |
| /** Page model this message listener is related to. */ |
| private final PageModel pageModel; |
| /** Context of the page this listener is related to. */ |
| private final Lookup pageContext; |
| |
| /** |
| * Creates a new {@code InspectionMessageListener}. |
| * |
| * @param pageModel page model the listener is related to. |
| * @param pageContext context of the page. |
| */ |
| InspectionMessageListener(PageModel pageModel, Lookup pageContext) { |
| this.pageModel = pageModel; |
| this.pageContext = pageContext; |
| } |
| |
| @Override |
| public void messageReceived(String featureId, final String messageTxt) { |
| if ((messageTxt == null) && !PageInspector.MESSAGE_DISPATCHER_FEATURE_ID.equals(featureId)) { |
| return; |
| } |
| if (messageTxt == null) { |
| // Invoke the cancelation of the inspection synchronously. |
| // The transport would be detached sooner otherwise. |
| processMessage(messageTxt); |
| } else { |
| // When the message comes from the external browser then |
| // this method is called in the thread that is processing all |
| // messages from the WebSocket server. We have to avoid blocking |
| // of this thread => we process the message in another thread. |
| RP.post(new Runnable() { |
| @Override |
| public void run() { |
| processMessage(messageTxt); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Processes incoming message for the inspected page. |
| * |
| * @param messageTxt message to process. |
| */ |
| private void processMessage(final String messageTxt) { |
| if (messageTxt == null) { |
| synchronized (LOCK) { |
| uninitSelectionMode(pageContext.lookup(JToolBar.class)); |
| if (pageModel == getPage()) { |
| inspectPage(Lookup.EMPTY); |
| } |
| } |
| } else { |
| try { |
| JSONObject message = (JSONObject)JSONValue.parseWithException(messageTxt); |
| Object type = message.get(MESSAGE_TYPE); |
| // Message about selection mode modification |
| if (MESSAGE_SELECTION_MODE.equals(type)) { |
| boolean selectionMode = (Boolean)message.get(MESSAGE_SELECTION_MODE_ATTR); |
| pageModel.setSelectionMode(selectionMode); |
| } |
| } catch (ParseException ex) { |
| Logger.getLogger(PageInspectorImpl.class.getName()) |
| .log(Level.INFO, "Ignoring message that is not in JSON format: {0}", messageTxt); // NOI18N |
| } |
| } |
| } |
| |
| } |
| |
| } |