package org.zaproxy.zap.view;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.log4j.Logger;
import org.parosproxy.paros.extension.AbstractPanel;
import org.parosproxy.paros.extension.option.OptionsParamView;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.view.TabbedPanel;
import org.zaproxy.zap.utils.DisplayUtils;
/**
* A tabbed panel that adds the option to hide individual tabs via a cross button on the tab.
*/
public class TabbedPanel2 extends TabbedPanel {
private static final long serialVersionUID = 1L;
private List<Component> fullTabList = new ArrayList<>();
private List<Component> removedTabList = new ArrayList<>();
private static final Icon PLUS_ICON = DisplayUtils.getScaledIcon(new ImageIcon(
TabbedPanel2.class.getResource("/resource/icon/fugue/plus.png")));
// A fake component that never actually get displayed - used for the 'hidden tab list tab'
private Component hiddenComponent = new JLabel();
private final Logger logger = Logger.getLogger(TabbedPanel2.class);
private int prevTabIndex = -1;
public TabbedPanel2() {
super();
this.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
setCloseButtonStates();
if (getSelectedComponent() != null && getSelectedComponent().equals(hiddenComponent)) {
// The 'hidden tab list tab' has been selected - this is a special case
if (prevTabIndex == indexOfComponent(hiddenComponent)) {
// Happens when we delete the tab to the left of the hidden one
setSelectedIndex(prevTabIndex-1);
} else {
// Hidden tab list tab selected - show popup and select previous tab
setSelectedIndex(prevTabIndex);
showHiddenTabPopup();
}
} else {
prevTabIndex = getSelectedIndex();
}
}
});
}
/**
* Show a popup containing a list of all of the hidden tabs - selecting one will reveal that tab
*/
private void showHiddenTabPopup() {
JPopupMenu menu = new JPopupMenu();
if (getMousePosition() == null) {
// Startup
return;
}
// Sort the list so the tabs are always in alphabetic order
Collections.sort(this.removedTabList, new Comparator<Component>(){
@Override
public int compare(Component o1, Component o2) {
return o1.getName().compareTo(o2.getName());
}});
for (Component c : this.removedTabList) {
if (c instanceof AbstractPanel) {
final AbstractPanel ap = (AbstractPanel)c;
JMenuItem mi = new JMenuItem(ap.getName());
mi.setIcon(ap.getIcon());
mi.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
setVisible(ap, true);
ap.setTabFocus();
}});
menu.add(mi);
}
}
menu.show(this, this.getMousePosition().x, this.getMousePosition().y);
}
/**
* Clones the given tabbed panel.
*
* @param tabbedPanel the tabbed panel to clone
* @return the cloned tabbed panel
* @deprecated (2.5.0) The implementation is not correct, not all state is correctly cloned.
*/
@Deprecated
public TabbedPanel2 clone(TabbedPanel2 tabbedPanel) {
TabbedPanel2 t = new TabbedPanel2();
t.fullTabList = new ArrayList<>(tabbedPanel.fullTabList);
t.removedTabList = new ArrayList<>(tabbedPanel.removedTabList);
return t;
}
private void setCloseButtonStates() {
// Hide all 'close' buttons except for the selected tab
for (int i = 0; i < this.getTabCount(); i++) {
Component tabCom = this.getTabComponentAt(i);
if (tabCom != null && tabCom instanceof TabbedPanelTab) {
TabbedPanelTab jp = (TabbedPanelTab) tabCom;
jp.setEnabled(i == getSelectedIndex());
}
}
}
public void pinVisibleTabs() {
for (int i = 0; i < this.getTabCount(); i++) {
Component tabCom = this.getTabComponentAt(i);
if (tabCom != null && tabCom instanceof TabbedPanelTab && tabCom.isVisible()) {
TabbedPanelTab jp = (TabbedPanelTab) tabCom;
jp.setPinned(true);
this.saveTabState(jp.getAbstractPanel());
}
}
}
public void unpinTabs() {
for (int i = 0; i < this.getTabCount(); i++) {
Component tabCom = this.getTabComponentAt(i);
if (tabCom != null && tabCom instanceof TabbedPanelTab && tabCom.isVisible()) {
TabbedPanelTab jp = (TabbedPanelTab) tabCom;
jp.setPinned(false);
this.saveTabState(jp.getAbstractPanel());
}
}
}
/**
* Returns a name safe to be used in the XML config file.
*
* @param str the name to be made safe
* @return a name safe to be used in XML
*/
private String safeName(String str) {
return str.replaceAll("[^A-Za-z0-9]", "");
}
private boolean isTabPinned(Component c) {
boolean showByDefault = false;
if (c instanceof AbstractPanel) {
showByDefault = ((AbstractPanel)c).isShowByDefault();
}
return Model.getSingleton().getOptionsParam().getConfig().getBoolean(
OptionsParamView.TAB_PIN_OPTION + "." + safeName(c.getName()), showByDefault);
}
protected void saveTabState(AbstractPanel ap) {
if (ap == null) {
return;
}
Model.getSingleton().getOptionsParam().getConfig().setProperty(
OptionsParamView.TAB_PIN_OPTION + "." + safeName(ap.getName()), ap.isPinned());
try {
Model.getSingleton().getOptionsParam().getConfig().save();
} catch (ConfigurationException e) {
logger.error(e.getMessage(), e);
}
}
/*
* Returns true if the specified component is a visible tab panel (typically an AbstractPanel)
*/
public boolean isTabVisible(Component c) {
if (! this.fullTabList.contains(c)) {
// Not a known tab
return false;
}
return ! this.removedTabList.contains(c);
}
public void setVisible(Component c, boolean visible) {
if (visible) {
if (this.removedTabList.contains(c)) {
if (c instanceof AbstractPanel) {
// Dont use the addTab(AbstractPanel) methods as we need to force visibility
AbstractPanel panel = (AbstractPanel)c;
this.addTab(c.getName(), panel.getIcon(), panel, panel.isHideable(), true, panel.getTabIndex());
} else {
// Work out the index to add it back in
int index = this.fullTabList.indexOf(c);
while (index >= 0) {
if (index > 0 && ! this.removedTabList.contains(this.fullTabList.get(index -1))) {
// Found the first preceding tab that isnt hidden
break;
}
index--;
}
this.addTab(c.getName(), null, c, true, true, index);
}
this.removedTabList.remove(c);
}
} else {
if (! this.removedTabList.contains(c)) {
remove(c);
this.removedTabList.add(c);
}
}
handleHiddenTabListTab();
}
@Override
public void addTab(String title, Icon icon, final Component c) {
if (c instanceof AbstractPanel) {
this.addTab((AbstractPanel)c);
} else {
this.addTab(title, icon, c, false, true, this.getTabCount());
}
}
public void addTab(AbstractPanel panel) {
boolean visible = ! panel.isHideable() || this.isTabPinned(panel);
this.addTab(panel.getName(), panel.getIcon(), panel, panel.isHideable(), visible, panel.getTabIndex());
}
public void addTab(String title, Icon icon, final Component c, boolean hideable, boolean visible, int index) {
if (c instanceof AbstractPanel) {
((AbstractPanel)c).setParent(this);
((AbstractPanel)c).setTabIndex(index);
((AbstractPanel)c).setHideable(hideable);
}
if (index == -1 || index > this.getTabCount()) {
index = this.getTabCount();
}
if (icon instanceof ImageIcon) {
icon = DisplayUtils.getScaledIcon((ImageIcon)icon);
}
super.insertTab(title, icon, c, c.getName(), index);
if ( ! this.fullTabList.contains(c)) {
this.fullTabList.add(c);
}
int pos = this.indexOfComponent(c);
// Now assign the component for the tab
this.setTabComponentAt(pos, new TabbedPanelTab(this, title, icon, c, hideable, this.isTabPinned(c)));
if (! visible) {
setVisible(c, false);
} else {
this.removedTabList.remove(c);
}
handleHiddenTabListTab();
if ((index == 0 || getTabCount() == 1) && indexOfComponent(c) != -1) {
// Its now the first one, give it focus
setSelectedComponent(c);
}
}
private void handleHiddenTabListTab() {
if (indexOfComponent(hiddenComponent) >= 0) {
// Tab is showing, remove it - it might not be needed or may no longer be at the end
super.remove(hiddenComponent);
}
if (this.removedTabList.size() > 0) {
// Only re-add tab if there are hidden ones
super.addTab("", PLUS_ICON, hiddenComponent);
}
}
/**
* Temporarily locks/unlocks the specified tab, eg if its active and mustn't be closed.
* <p>
* Locked (AbstractPanel) tabs will not have the pin/close tab buttons displayed.
*
* @param panel the panel being changed
* @param lock {@code true} if the panel should be locked, {@code false} otherwise.
*/
public void setTabLocked(AbstractPanel panel, boolean lock) {
for (int i = 0; i < this.getTabCount(); i++) {
Component tabCom = this.getTabComponentAt(i);
if (tabCom != null && tabCom instanceof TabbedPanelTab && tabCom.isVisible()) {
TabbedPanelTab jp = (TabbedPanelTab) tabCom;
if (panel.equals(jp.getAbstractPanel())) {
jp.setLocked(!lock);
}
}
}
}
@Override
public void setIconAt(int index, Icon icon) {
Component tabCom = this.getTabComponentAt(index);
if (tabCom != null && tabCom instanceof JPanel) {
Component c = ((JPanel)tabCom).getComponent(0);
if (c != null && c instanceof JLabel) {
((JLabel)c).setIcon(icon);
}
}
}
/**
* Set the title of the tab when hiding/showing tab names.
*/
@Override
public void setTitleAt(int index, String title) {
Component tabCom = this.getTabComponentAt(index);
if (tabCom != null && tabCom instanceof JPanel) {
Component c = ((JPanel)tabCom).getComponent(0);
if (c != null && c instanceof JLabel) {
((JLabel)c).setText(title);
}
}
else {
super.setTitleAt(index, title);
}
}
public List<Component> getTabList() {
return Collections.unmodifiableList(this.fullTabList);
}
public List<Component> getSortedTabList() {
List<Component> copy = new ArrayList<Component>(this.fullTabList);
Collections.sort(copy, new Comparator<Component>(){
@Override
public int compare(Component o1, Component o2) {
return o1.getName().compareTo(o2.getName());
}});
return copy;
}
public void removeTab(AbstractPanel panel) {
this.remove(panel);
this.fullTabList.remove(panel);
if (this.removedTabList.remove(panel)) {
handleHiddenTabListTab();
}
}
@Override
public void removeAll() {
super.removeAll();
removedTabList.clear();
removedTabList.addAll(fullTabList);
handleHiddenTabListTab();
}
/**
* Sets whether or not the tab names should be shown.
*
* @param showTabNames {@code true} if the tab names should be shown, {@code false} otherwise.
* @since 2.4.0
*/
public void setShowTabNames(boolean showTabNames) {
for (int i = 0; i < getTabCount(); i++) {
String title = showTabNames ? getComponentAt(i).getName() : "";
setTitleAt(i, title);
}
}
/**
* {@inheritDoc}
* <p>
* Overridden to call the method {@code AbstractPanel#tabSelected()} on the currently selected {@code AbstractPanel}, if
* any.
*
* @see AbstractPanel#tabSelected()
*/
@Override
protected void fireStateChanged() {
super.fireStateChanged();
Component comp = getSelectedComponent();
if (comp instanceof AbstractPanel) {
((AbstractPanel) comp).tabSelected();
}
}
/**
* Returns true if the tab is 'active' - ie is being used for anything.
* This method always returns false so must be overriden to be changed
*
* @return {@code true} if the tab is active, {@code false} otherwise
*/
public boolean isActive() {
return false;
}
/**
* Gets all the {@code AbstractPanel}s.
*
* @return a {@code List} containing all the panels
* @since 2.5.0
* @see #getVisiblePanels()
*/
public List<AbstractPanel> getPanels() {
List<AbstractPanel> panels = new ArrayList<>();
for (Component component : fullTabList) {
if (component instanceof AbstractPanel) {
panels.add((AbstractPanel) component);
}
}
return panels;
}
/**
* Gets all the {@code AbstractPanel}s that are currently visible.
*
* @return a {@code List} containing all the visible panels
* @since 2.5.0
* @see #getPanels()
*/
public List<AbstractPanel> getVisiblePanels() {
List<AbstractPanel> panels = getPanels();
for (Iterator<AbstractPanel> it = panels.iterator(); it.hasNext();) {
if (removedTabList.contains(it.next())) {
it.remove();
}
}
return panels;
}
/**
* Sets the given {@code panels} as visible, while hiding the remaining panels.
* <p>
* Any panel that cannot be hidden (per {@link AbstractPanel#isHideable()} and {@link AbstractPanel#isPinned()}) will still
* be shown, even if the panel was not in the given {@code panels}, moreover {@code panels} that are not currently added to
* this tabbed panel are ignored.
*
* @param panels the panels that should be visible
* @since 2.5.0
* @see #getVisiblePanels()
*/
public void setVisiblePanels(List<AbstractPanel> panels) {
removeAll();
for (Component component : fullTabList) {
if (panels.contains(component)) {
setVisible(component, true);
} else if (component instanceof AbstractPanel) {
AbstractPanel ap = (AbstractPanel) component;
if (!canHidePanel(ap)) {
setVisible(component, true);
}
}
}
if (getSelectedComponent() == null && getTabCount() > 0) {
setSelectedIndex(0);
}
}
/**
* Sets whether or not the panels should be visible.
* <p>
* {@link AbstractPanel#isHideable() Non-hideable} and {@link AbstractPanel#isPinned() pinned} panels are not affected by
* this call, when set to not be visible.
*
* @param visible {@code true} if all panels should be visible, {@code false} otherwise.
* @since 2.5.0
* @see #getVisiblePanels()
*/
public void setPanelsVisible(boolean visible) {
for (Component component : fullTabList) {
if (component instanceof AbstractPanel) {
AbstractPanel ap = (AbstractPanel) component;
boolean canChangeVisibility = true;
if (!visible) {
canChangeVisibility = canHidePanel(ap);
}
if (canChangeVisibility) {
setVisible(component, visible);
}
}
}
}
/**
* Tells whether or not the given panel can be hidden.
* <p>
* A panel can be hidden if it is {@link AbstractPanel#isHideable() hideable} and it's not {@link AbstractPanel#isPinned()
* pinned}.
*
* @param panel the panel to be checked
* @return {@code true} if the panel can be hidden, {@code false} otherwise
*/
private static boolean canHidePanel(AbstractPanel panel) {
return panel.isHideable() && !panel.isPinned();
}
}