/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2015 The ZAP Development Team * * Licensed 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.zaproxy.zap.view; import java.awt.Container; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.table.TableModel; import org.jdesktop.swingx.JXTable; import org.jdesktop.swingx.action.AbstractActionExt; import org.jdesktop.swingx.table.ColumnControlButton; import org.parosproxy.paros.Constant; import org.zaproxy.zap.utils.StickyScrollbarAdjustmentListener; /** * An enhanced {@code JXTable}. It has the following enhancements: * <ul> * <li>Ensures that the right-clicked row is selected before showing context menu (for installed JPopupMenu);</li> * <li>Allows to enable auto-scroll on new values when scroll bar is at bottom of the table (enabled by default);</li> * </ul> * * @since 2.4.1 * @see JXTable * @see #setComponentPopupMenu(JPopupMenu) * @see #setAutoScrollOnNewValues(boolean) */ public class ZapTable extends JXTable { private static final long serialVersionUID = 8303870012122236918L; private boolean autoScroll; private AutoScrollAction autoScrollAction; private StickyScrollbarAdjustmentListener autoScrollScrollbarAdjustmentListener; public ZapTable() { super(); init(); } public ZapTable(TableModel dataModel) { super(dataModel); init(); } private void init() { setDoubleBuffered(true); setColumnControlVisible(true); JComponent columnControl = getColumnControl(); if (columnControl instanceof ZapColumnControlButton) { ZapColumnControlButton zapColumnControl = ((ZapColumnControlButton) columnControl); zapColumnControl.addAction(getAutoScrollAction()); zapColumnControl.populatePopup(); } setAutoScrollOnNewValues(true); } protected AutoScrollAction getAutoScrollAction() { if (autoScrollAction == null) { autoScrollAction = new AutoScrollAction(this); } return autoScrollAction; } /** * Sets if the vertical scroll bar of the wrapper {@code JScrollPane} should be automatically scrolled on new values. * <p> * Default value is to {@code true}. * * @param autoScroll {@code true} if vertical scroll bar should be automatically scrolled on new values, {@code false} * otherwise. */ public void setAutoScrollOnNewValues(boolean autoScroll) { if (this.autoScroll == autoScroll) { return; } if (this.autoScroll) { removeAutoScrollScrollbarAdjustmentListener(); } this.autoScroll = autoScroll; if (autoScrollAction != null) { autoScrollAction.putValue(Action.SELECTED_KEY, Boolean.valueOf(autoScroll)); } if (this.autoScroll) { addAutoScrollScrollbarAdjustmentListener(); } } /** * Tells whether or not the vertical scroll bar of the wrapper {@code JScrollPane} is automatically scrolled on new values. * * @return {@code true} if the vertical scroll bar is automatically scrolled on new values, {@code false} otherwise. * @see #setAutoScrollOnNewValues(boolean) */ public boolean isAutoScrollOnNewValues() { return autoScroll; } private void addAutoScrollScrollbarAdjustmentListener() { JScrollPane scrollPane = getEnclosingScrollPane(); if (scrollPane != null && autoScrollScrollbarAdjustmentListener == null) { autoScrollScrollbarAdjustmentListener = new StickyScrollbarAdjustmentListener(); scrollPane.getVerticalScrollBar().addAdjustmentListener(autoScrollScrollbarAdjustmentListener); } } private void removeAutoScrollScrollbarAdjustmentListener() { JScrollPane scrollPane = getEnclosingScrollPane(); if (scrollPane != null && autoScrollScrollbarAdjustmentListener != null) { scrollPane.getVerticalScrollBar().removeAdjustmentListener(autoScrollScrollbarAdjustmentListener); autoScrollScrollbarAdjustmentListener = null; } } /** * {@inheritDoc} * <p> * Overridden to set auto-scroll on new values, if enabled. * </p> */ @Override protected void configureEnclosingScrollPane() { super.configureEnclosingScrollPane(); if (isAutoScrollOnNewValues()) { addAutoScrollScrollbarAdjustmentListener(); } } /** * {@inheritDoc} * <p> * Overridden to unset auto-scroll on new values, if enabled. * </p> */ @Override protected void unconfigureEnclosingScrollPane() { super.unconfigureEnclosingScrollPane(); if (isAutoScrollOnNewValues()) { removeAutoScrollScrollbarAdjustmentListener(); } } /** * {@inheritDoc} * <p> * Overridden to take into account for possible parent {@code JLayer}s. * </p> * * @see javax.swing.JLayer */ // Note: Same implementation as in JXTable#getEnclosingScrollPane() but changed to get the parent and viewport view using // the methods SwingUtilities#getUnwrappedParent(Component) and SwingUtilities#getUnwrappedView(JViewport) respectively. @Override protected JScrollPane getEnclosingScrollPane() { Container p = SwingUtilities.getUnwrappedParent(this); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane) gp; // Make certain we are the viewPort's view and not, for // example, the rowHeaderView of the scrollPane - // an implementor of fixed columns might do this. JViewport viewport = scrollPane.getViewport(); if (viewport == null || SwingUtilities.getUnwrappedView(viewport) != this) { return null; } return scrollPane; } } return null; } @Override public Point getPopupLocation(final MouseEvent event) { // Hack to select the row before showing the pop up menu when invoked using the mouse. if (event != null) { final int row = rowAtPoint(event.getPoint()); if (row < 0) { getSelectionModel().clearSelection(); } else if (!getSelectionModel().isSelectedIndex(row)) { getSelectionModel().setSelectionInterval(row, row); } } return super.getPopupLocation(event); } @Override protected JComponent createDefaultColumnControl() { return new ZapColumnControlButton(this); } protected static class ZapColumnControlButton extends ColumnControlButton { private static final long serialVersionUID = -2888568545235496369L; private List<Action> customActions; public ZapColumnControlButton(JXTable table) { super(table); } public ZapColumnControlButton(JXTable table, Icon icon) { super(table, icon); } @Override public void populatePopup() { super.populatePopup(); if (customActions != null && popup instanceof DefaultColumnControlPopup) { ((DefaultColumnControlPopup) popup).addAdditionalActionItems(customActions); } } public void addAction(Action action) { if (customActions == null) { customActions = new ArrayList<>(1); } customActions.add(action); } } protected static class AutoScrollAction extends AbstractActionExt { private static final long serialVersionUID = 5518182106427836717L; private final ZapTable table; public AutoScrollAction(ZapTable table) { super(Constant.messages.getString("view.table.autoscroll.label")); putValue(Action.SHORT_DESCRIPTION, Constant.messages.getString("view.table.autoscroll.tooltip")); this.table = table; } public AutoScrollAction(String label, Icon icon, ZapTable table) { super(label, icon); this.table = table; } @Override public boolean isStateAction() { return true; } @Override public void actionPerformed(ActionEvent e) { table.setAutoScrollOnNewValues(!table.isAutoScrollOnNewValues()); } } }