blob: b41b01cb3b7d96417a8752f55ff1a3ec1d0fbea9 [file] [log] [blame]
/*
* 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
}
}
}
}
}