/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * 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 net.java.sip.communicator.plugin.desktoputil.plaf; /* * The content of this file was based on code borrowed from David Bismut, * davidou@mageos.com Intern, SETLabs, Infosys Technologies Ltd. May 2004 - Jul * 2004 Ecole des Mines de Nantes, France */ import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.util.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.*; import javax.swing.plaf.*; import javax.swing.plaf.basic.*; import javax.swing.text.*; import net.java.sip.communicator.plugin.desktoputil.*; import net.java.sip.communicator.util.skin.*; /** * SIPCommTabbedPaneUI implementation. */ public class SIPCommTabbedPaneUI extends BasicTabbedPaneUI implements Skinnable { /** * The image used in the <tt>SIPCommLookAndFeel</tt> to paint a close * button on a tab. */ private static final String CLOSE_TAB_ICON = "service.gui.lookandfeel.CLOSE_TAB_ICON"; /** * The image used in the <tt>SIPCommLookAndFeel</tt> to paint a rollover * close button on a tab. */ //private static final String CLOSE_TAB_SELECTED_ICON = // "service.gui.lookandfeel.CLOSE_TAB_SELECTED_ICON"; // Instance variables initialized at installation private ContainerListener containerListener; private Vector<View> htmlViews; private Map<Integer, Integer> mnemonicToIndexMap; /** * InputMap used for mnemonics. Only non-null if the JTabbedPane has * mnemonics associated with it. Lazily created in initMnemonics. */ private InputMap mnemonicInputMap; // For use when tabLayoutPolicy = SCROLL_TAB_LAYOUT protected ScrollableTabSupport tabScroller; private int tabCount; protected MyMouseMotionListener motionListener; // UI creation private static final int INACTIVE = 0; private static final int OVER = 1; private static final int PRESSED = 2; public static final int BUTTONSIZE = 15; public static final int WIDTHDELTA = 1; private static final Border PRESSEDBORDER = new SoftBevelBorder( SoftBevelBorder.LOWERED); private static final Border OVERBORDER = new SoftBevelBorder( SoftBevelBorder.RAISED); //private Image closeImgB; //private BufferedImage maxImgB; private Image closeImgI; private BufferedImage maxImgI; private int overTabIndex = -1; private int closeIndexStatus = INACTIVE; private int maxIndexStatus = INACTIVE; private boolean mousePressed = false; private boolean isCloseButtonEnabled = false; private boolean isMaxButtonEnabled = false; protected JPopupMenu actionPopupMenu; protected JMenuItem maxItem; protected JMenuItem closeItem; public SIPCommTabbedPaneUI() { //closeImgB = SwingSwingUtilActivator.getImage(CLOSE_TAB_SELECTED_ICON); //maxImgB = new BufferedImage(BUTTONSIZE, BUTTONSIZE, // BufferedImage.TYPE_4BYTE_ABGR); loadSkin(); actionPopupMenu = new JPopupMenu(); maxItem = new JMenuItem("Detach"); closeItem = new JMenuItem("Close"); maxItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ((SIPCommTabbedPane) tabPane).fireMaxTabEvent(null, tabPane .getSelectedIndex()); } }); closeItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ((SIPCommTabbedPane) tabPane).fireCloseTabEvent(null, tabPane .getSelectedIndex()); } }); setPopupMenu(); } protected boolean isOneActionButtonEnabled() { return isCloseButtonEnabled || isMaxButtonEnabled; } public boolean isCloseEnabled() { return isCloseButtonEnabled; } public boolean isMaxEnabled() { return isMaxButtonEnabled; } public void setCloseIcon(boolean b) { isCloseButtonEnabled = b; setPopupMenu(); } public void setMaxIcon(boolean b) { isMaxButtonEnabled = b; setPopupMenu(); } private void setPopupMenu() { actionPopupMenu.removeAll(); if (isMaxButtonEnabled) actionPopupMenu.add(maxItem); if (isMaxButtonEnabled && isCloseButtonEnabled) actionPopupMenu.addSeparator(); if (isCloseButtonEnabled) actionPopupMenu.add(closeItem); } @Override protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) { int delta = 0; Insets tabInsets = getTabInsets(tabPlacement, tabIndex); if (isOneActionButtonEnabled()) { tabInsets.right = 0; if (isCloseButtonEnabled) delta += BUTTONSIZE; if (isMaxButtonEnabled) delta += BUTTONSIZE; } return super.calculateTabWidth(tabPlacement, tabIndex, metrics) + delta; } @Override protected int calculateTabHeight(int tabPlacement, int tabIndex, int fontHeight) { return super.calculateTabHeight(tabPlacement, tabIndex, fontHeight + 4); } @Override protected void layoutLabel(int tabPlacement, FontMetrics metrics, int tabIndex, String title, Icon icon, Rectangle tabRect, Rectangle iconRect, Rectangle textRect, boolean isSelected) { textRect.x = textRect.y = iconRect.x = iconRect.y = 0; View v = getTextViewForTab(tabIndex); if (v != null) { tabPane.putClientProperty("html", v); } SwingUtilities.layoutCompoundLabel(tabPane, metrics, title, icon, SwingUtilities.CENTER, SwingUtilities.LEFT, SwingUtilities.CENTER, SwingUtilities.CENTER, tabRect, iconRect, textRect, 0); tabPane.putClientProperty("html", null); if (icon != null) { iconRect.y = iconRect.y + 2; iconRect.x = tabRect.x + 7; } textRect.y = textRect.y + 2; if (icon != null) textRect.x = iconRect.x + iconRect.width + 5; else textRect.x = textRect.x + 8; } @Override protected MouseListener createMouseListener() { return new MyMouseHandler(); } protected ScrollableTabButton createScrollableTabButton(int direction) { return new ScrollableTabButton(direction); } protected Rectangle newCloseRect(Rectangle rect) { int dx = rect.x + rect.width - BUTTONSIZE - WIDTHDELTA; int dy = rect.y + (rect.height - BUTTONSIZE) / 2 + 2; return new Rectangle(dx, dy, BUTTONSIZE, BUTTONSIZE); } protected Rectangle newMaxRect(Rectangle rect) { int dx = rect.x + rect.width - BUTTONSIZE - WIDTHDELTA; int dy = rect.y + (rect.height - BUTTONSIZE) / 2 + 2; if (isCloseButtonEnabled) dx -= BUTTONSIZE; return new Rectangle(dx, dy, BUTTONSIZE, BUTTONSIZE); } protected void updateOverTab(int x, int y) { int overTabIndex = getTabAtLocation(x, y); if (this.overTabIndex != overTabIndex) { this.overTabIndex = overTabIndex; tabScroller.tabPanel.repaint(); } } protected void updateCloseIcon(int x, int y) { if (overTabIndex != -1) { int newCloseIndexStatus = INACTIVE; Rectangle closeRect = newCloseRect(rects[overTabIndex]); if (closeRect.contains(x, y)) newCloseIndexStatus = mousePressed ? PRESSED : OVER; if (closeIndexStatus != newCloseIndexStatus) { closeIndexStatus = newCloseIndexStatus; tabScroller.tabPanel.repaint(); } } } protected void updateMaxIcon(int x, int y) { if (overTabIndex != -1) { int newMaxIndexStatus = INACTIVE; Rectangle maxRect = newMaxRect(rects[overTabIndex]); if (maxRect.contains(x, y)) newMaxIndexStatus = mousePressed ? PRESSED : OVER; if (maxIndexStatus != newMaxIndexStatus) { maxIndexStatus = newMaxIndexStatus; tabScroller.tabPanel.repaint(); } } } private void setTabIcons(int x, int y) { // if the mouse isPressed if (!mousePressed) updateOverTab(x, y); if (isCloseButtonEnabled) updateCloseIcon(x, y); if (isMaxButtonEnabled) updateMaxIcon(x, y); } public static ComponentUI createUI(JComponent c) { return new SIPCommTabbedPaneUI(); } /** * Invoked by <code>installUI</code> to create a layout manager object to * manage the <code>JTabbedPane</code>. * * @return a layout manager object * * @see javax.swing.JTabbedPane#getTabLayoutPolicy */ @Override protected LayoutManager createLayoutManager() { return new TabbedPaneScrollLayout(); } /* * In an attempt to preserve backward compatibility for programs which have * extended BasicTabbedPaneUI to do their own layout, the UI uses the * installed layoutManager (and not tabLayoutPolicy) to determine if * scrollTabLayout is enabled. */ /** * Creates and installs any required subcomponents for the JTabbedPane. * Invoked by installUI. * * @since 1.4 */ @Override protected void installComponents() { if (tabScroller == null) { tabScroller = new ScrollableTabSupport(tabPane.getTabPlacement()); tabPane.add(tabScroller.viewport); tabPane.add(tabScroller.scrollForwardButton); tabPane.add(tabScroller.scrollBackwardButton); } } /** * Removes any installed subcomponents from the JTabbedPane. Invoked by * uninstallUI. * * @since 1.4 */ @Override protected void uninstallComponents() { tabPane.remove(tabScroller.viewport); tabPane.remove(tabScroller.scrollForwardButton); tabPane.remove(tabScroller.scrollBackwardButton); tabScroller = null; } @Override protected void installListeners() { if ((propertyChangeListener = createPropertyChangeListener()) != null) { tabPane.addPropertyChangeListener(propertyChangeListener); } if ((tabChangeListener = createChangeListener()) != null) { tabPane.addChangeListener(tabChangeListener); } if ((mouseListener = createMouseListener()) != null) { tabScroller.tabPanel.addMouseListener(mouseListener); } if ((focusListener = createFocusListener()) != null) { tabPane.addFocusListener(focusListener); } // PENDING(api) : See comment for ContainerHandler if ((containerListener = new ContainerHandler()) != null) { tabPane.addContainerListener(containerListener); if (tabPane.getTabCount() > 0) { htmlViews = createHTMLVector(); } } if ((motionListener = new MyMouseMotionListener()) != null) { tabScroller.tabPanel.addMouseMotionListener(motionListener); } } @Override protected void uninstallListeners() { if (mouseListener != null) { tabScroller.tabPanel.removeMouseListener(mouseListener); mouseListener = null; } if (motionListener != null) { tabScroller.tabPanel.removeMouseMotionListener(motionListener); motionListener = null; } if (focusListener != null) { tabPane.removeFocusListener(focusListener); focusListener = null; } // PENDING(api): See comment for ContainerHandler if (containerListener != null) { tabPane.removeContainerListener(containerListener); containerListener = null; if (htmlViews != null) { htmlViews.removeAllElements(); htmlViews = null; } } if (tabChangeListener != null) { tabPane.removeChangeListener(tabChangeListener); tabChangeListener = null; } if (propertyChangeListener != null) { tabPane.removePropertyChangeListener(propertyChangeListener); propertyChangeListener = null; } } @Override protected ChangeListener createChangeListener() { return new TabSelectionHandler(); } @Override protected void installKeyboardActions() { InputMap km = getMyInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); SwingUtilities.replaceUIInputMap(tabPane, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, km); km = getMyInputMap(JComponent.WHEN_FOCUSED); SwingUtilities.replaceUIInputMap(tabPane, JComponent.WHEN_FOCUSED, km); ActionMap am = createMyActionMap(); SwingUtilities.replaceUIActionMap(tabPane, am); tabScroller.scrollForwardButton.setAction(am .get("scrollTabsForwardAction")); tabScroller.scrollBackwardButton.setAction(am .get("scrollTabsBackwardAction")); } InputMap getMyInputMap(int condition) { if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) { return (InputMap) UIManager.get("TabbedPane.ancestorInputMap"); } else if (condition == JComponent.WHEN_FOCUSED) { return (InputMap) UIManager.get("TabbedPane.focusInputMap"); } return null; } ActionMap createMyActionMap() { ActionMap map = new ActionMapUIResource(); map.put("navigateNext", new DirectionAction(NEXT)); map.put("navigatePrevious", new DirectionAction(PREVIOUS)); map.put("navigateRight", new DirectionAction(EAST)); map.put("navigateLeft", new DirectionAction(WEST)); map.put("navigateUp", new DirectionAction(NORTH)); map.put("navigateDown", new DirectionAction(SOUTH)); map.put("navigatePageUp", new PageAction(true)); map.put("navigatePageDown", new PageAction(false)); map.put("requestFocus", new RequestFocusAction()); map.put("requestFocusForVisibleComponent", new RequestFocusForVisibleAction()); map.put("setSelectedIndex", new SetSelectedIndexAction()); map.put("scrollTabsForwardAction", new ScrollTabsForwardAction()); map.put("scrollTabsBackwardAction", new ScrollTabsBackwardAction()); return map; } @Override protected void uninstallKeyboardActions() { SwingUtilities.replaceUIActionMap(tabPane, null); SwingUtilities.replaceUIInputMap(tabPane, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, null); SwingUtilities .replaceUIInputMap(tabPane, JComponent.WHEN_FOCUSED, null); } /** * Reloads the mnemonics. This should be invoked when a memonic changes, * when the title of a mnemonic changes, or when tabs are added/removed. */ private void updateMnemonics() { resetMnemonics(); for (int counter = tabPane.getTabCount() - 1; counter >= 0; counter--) { int mnemonic = tabPane.getMnemonicAt(counter); if (mnemonic > 0) { addMnemonic(counter, mnemonic); } } } /** * Resets the mnemonics bindings to an empty state. */ private void resetMnemonics() { if (mnemonicToIndexMap != null) { mnemonicToIndexMap.clear(); mnemonicInputMap.clear(); } } /** * Adds the specified mnemonic at the specified index. */ private void addMnemonic(int index, int mnemonic) { if (mnemonicToIndexMap == null) { initMnemonics(); } mnemonicInputMap.put(KeyStroke.getKeyStroke(mnemonic, Event.ALT_MASK), "setSelectedIndex"); mnemonicToIndexMap.put(mnemonic, index); } /** * Installs the state needed for mnemonics. */ private void initMnemonics() { mnemonicToIndexMap = new Hashtable<Integer, Integer>(); mnemonicInputMap = new InputMapUIResource(); mnemonicInputMap.setParent(SwingUtilities.getUIInputMap(tabPane, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)); SwingUtilities .replaceUIInputMap(tabPane, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, mnemonicInputMap); } // UI Rendering @Override public void paint(Graphics g, JComponent c) { int tc = tabPane.getTabCount(); if (tabCount != tc) { tabCount = tc; updateMnemonics(); } int selectedIndex = tabPane.getSelectedIndex(); int tabPlacement = tabPane.getTabPlacement(); ensureCurrentLayout(); // Paint content border paintContentBorder(g, tabPlacement, selectedIndex); } @Override protected void paintTab(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect) { Rectangle tabRect = rects[tabIndex]; int selectedIndex = tabPane.getSelectedIndex(); boolean isSelected = selectedIndex == tabIndex; boolean isOver = overTabIndex == tabIndex; Graphics2D g2 = null; Shape save = null; boolean cropShape = false; int cropx = 0; int cropy = 0; if (g instanceof Graphics2D) { g2 = (Graphics2D) g; AntialiasingManager.activateAntialiasing(g2); // Render visual for cropped tab edge... Rectangle viewRect = tabScroller.viewport.getViewRect(); int cropline; cropline = viewRect.x + viewRect.width; if ((tabRect.x < cropline) && (tabRect.x + tabRect.width > cropline)) { cropx = cropline - 1; cropy = tabRect.y; cropShape = true; } if (cropShape) { save = g2.getClip(); g2.clipRect(tabRect.x, tabRect.y, tabRect.width, tabRect.height); } } paintTabBackground(g, tabPlacement, tabIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height, isSelected); paintTabBorder(g, tabPlacement, tabIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height, isSelected); String title = tabPane.getTitleAt(tabIndex); Font font = tabPane.getFont(); FontMetrics metrics = g.getFontMetrics(font); Icon icon = getIconForTab(tabIndex); layoutLabel(tabPlacement, metrics, tabIndex, title, icon, tabRect, iconRect, textRect, isSelected); paintText(g, tabPlacement, font, metrics, tabIndex, title, textRect, isSelected); paintIcon(g, tabPlacement, tabIndex, icon, iconRect, isSelected); paintFocusIndicator(g, tabPlacement, rects, tabIndex, iconRect, textRect, isSelected); if (cropShape) { paintCroppedTabEdge(g, tabPlacement, tabIndex, isSelected, cropx, cropy); g2.setClip(save); } else if (isOver || isSelected) { Rectangle closeRect = newCloseRect(tabRect); int dx = closeRect.x; int dy = closeRect.y; if (isCloseButtonEnabled) paintCloseIcon(g2, dx, dy, isOver); if (isMaxButtonEnabled) paintMaxIcon(g2, dx, dy, isOver); } } protected void paintCloseIcon(Graphics g, int dx, int dy, boolean isOver) { // paintActionButton(g, dx, dy, closeIndexStatus, isOver, closeB, // closeImgB); g.drawImage(closeImgI, dx, dy + 1, null); } protected void paintMaxIcon(Graphics g, int dx, int dy, boolean isOver) { if (isCloseButtonEnabled) dx -= BUTTONSIZE; // paintActionButton(g, dx, dy, maxIndexStatus, isOver, maxB, maxImgB); g.drawImage(maxImgI, dx, dy + 1, null); } protected void paintActionButton(Graphics g, int dx, int dy, int status, boolean isOver, JButton button, Image image) { button.setBorder(null); if (isOver) { switch (status) { case OVER: button.setBorder(OVERBORDER); break; case PRESSED: button.setBorder(PRESSEDBORDER); break; } } button.setBackground(tabScroller.tabPanel.getBackground()); button.paint(image.getGraphics()); g.drawImage(image, dx, dy, null); } /* * This method will create and return a polygon shape for the given tab * rectangle which has been cropped at the specified cropline with a torn * edge visual. e.g. A "File" tab which has cropped been cropped just after * the "i": ------------- | ..... | | . | | ... . | | . . | | . . | | . . | * -------------- * * The x, y arrays below define the pattern used to create a "torn" edge * segment which is repeated to fill the edge of the tab. For tabs placed on * TOP and BOTTOM, this righthand torn edge is created by line segments * which are defined by coordinates obtained by subtracting xCropLen[i] from * (tab.x + tab.width) and adding yCroplen[i] to (tab.y). For tabs placed on * LEFT or RIGHT, the bottom torn edge is created by subtracting xCropLen[i] * from (tab.y + tab.height) and adding yCropLen[i] to (tab.x). */ //private static final int CROP_SEGMENT = 12; private void paintCroppedTabEdge(Graphics g, int tabPlacement, int tabIndex, boolean isSelected, int x, int y) { g.setColor(shadow); g.drawLine(x, y, x, y + rects[tabIndex].height); } private void ensureCurrentLayout() { if (!tabPane.isValid()) { tabPane.validate(); } /* * If tabPane doesn't have a peer yet, the validate() call will silently * fail. We handle that by forcing a layout if tabPane is still invalid. * See bug 4237677. */ if (!tabPane.isValid()) { TabbedPaneLayout layout = (TabbedPaneLayout) tabPane.getLayout(); layout.calculateLayoutInfo(); } } /** * Returns the bounds of the specified tab in the coordinate space of the * JTabbedPane component. This is required because the tab rects are by * default defined in the coordinate space of the component where they are * rendered, which could be the JTabbedPane (for WRAP_TAB_LAYOUT) or a * ScrollableTabPanel (SCROLL_TAB_LAYOUT). This method should be used * whenever the tab rectangle must be relative to the JTabbedPane itself and * the result should be placed in a designated Rectangle object (rather than * instantiating and returning a new Rectangle each time). The tab index * parameter must be a valid tabbed pane tab index (0 to tab count - 1, * inclusive). The destination rectangle parameter must be a valid * <code>Rectangle</code> instance. The handling of invalid parameters is * unspecified. * * @param tabIndex * the index of the tab * @param dest * the rectangle where the result should be placed * @return the resulting rectangle * * @since 1.4 */ @Override protected Rectangle getTabBounds(int tabIndex, Rectangle dest) { dest.width = rects[tabIndex].width; dest.height = rects[tabIndex].height; Point vpp = tabScroller.viewport.getLocation(); Point viewp = tabScroller.viewport.getViewPosition(); dest.x = rects[tabIndex].x + vpp.x - viewp.x; dest.y = rects[tabIndex].y + vpp.y - viewp.y; return dest; } private int getTabAtLocation(int x, int y) { ensureCurrentLayout(); int tabCount = tabPane.getTabCount(); for (int i = 0; i < tabCount; i++) { if (rects[i].contains(x, y)) { return i; } } return -1; } public int getOverTabIndex() { return overTabIndex; } /** * Returns the index of the tab closest to the passed in location, note that * the returned tab may not contain the location x,y. */ protected int getClosestTab(int x, int y) { int min = 0; int tabCount = Math.min(rects.length, tabPane.getTabCount()); int max = tabCount; int tabPlacement = tabPane.getTabPlacement(); boolean useX = (tabPlacement == TOP || tabPlacement == BOTTOM); int want = (useX) ? x : y; while (min != max) { int current = (max + min) / 2; int minLoc; int maxLoc; if (useX) { minLoc = rects[current].x; maxLoc = minLoc + rects[current].width; } else { minLoc = rects[current].y; maxLoc = minLoc + rects[current].height; } if (want < minLoc) { max = current; if (min == max) { return Math.max(0, current - 1); } } else if (want >= maxLoc) { min = current; if (max - min <= 1) { return Math.max(current + 1, tabCount - 1); } } else { return current; } } return min; } /** * Returns a point which is translated from the specified point in the * JTabbedPane's coordinate space to the coordinate space of the * ScrollableTabPanel. This is used for SCROLL_TAB_LAYOUT ONLY. */ private Point translatePointToTabPanel(int srcx, int srcy, Point dest) { Point vpp = tabScroller.viewport.getLocation(); Point viewp = tabScroller.viewport.getViewPosition(); dest.x = srcx + vpp.x + viewp.x; dest.y = srcy + vpp.y + viewp.y; return dest; } // BasicTabbedPaneUI methods // Tab Navigation methods // REMIND(aim,7/29/98): This method should be made // protected in the next release where // API changes are allowed // boolean requestMyFocusForVisibleComponent() { Component visibleComponent = getVisibleComponent(); if (visibleComponent.isFocusable()) { visibleComponent.requestFocus(); return true; } else if (visibleComponent instanceof JComponent) { if (((JComponent) visibleComponent).requestFocusInWindow()) { return true; } } return false; } private static class DirectionAction extends AbstractAction { /** * Serial version UID. */ private static final long serialVersionUID = 0L; private final int direction; public DirectionAction(int direction) { this.direction = direction; } public void actionPerformed(ActionEvent e) { JTabbedPane pane = (JTabbedPane) e.getSource(); SIPCommTabbedPaneUI ui = (SIPCommTabbedPaneUI) pane.getUI(); ui.navigateSelectedTab(direction); } }; private static class PageAction extends AbstractAction { /** * Serial version UID. */ private static final long serialVersionUID = 0L; private final boolean up; public PageAction(boolean up) { this.up = up; } public void actionPerformed(ActionEvent e) { JTabbedPane pane = (JTabbedPane) e.getSource(); SIPCommTabbedPaneUI ui = (SIPCommTabbedPaneUI) pane.getUI(); int tabPlacement = pane.getTabPlacement(); if (tabPlacement == TOP || tabPlacement == BOTTOM) { ui.navigateSelectedTab(up ? WEST : EAST); } else { ui.navigateSelectedTab(up ? NORTH : SOUTH); } } }; private static class RequestFocusAction extends AbstractAction { private static final long serialVersionUID = 0L; public void actionPerformed(ActionEvent e) { JTabbedPane pane = (JTabbedPane) e.getSource(); pane.requestFocus(); } }; private static class RequestFocusForVisibleAction extends AbstractAction { private static final long serialVersionUID = 0L; public void actionPerformed(ActionEvent e) { JTabbedPane pane = (JTabbedPane) e.getSource(); SIPCommTabbedPaneUI ui = (SIPCommTabbedPaneUI) pane.getUI(); ui.requestMyFocusForVisibleComponent(); } }; /** * Selects a tab in the JTabbedPane based on the String of the action * command. The tab selected is based on the first tab that has a mnemonic * matching the first character of the action command. */ private static class SetSelectedIndexAction extends AbstractAction { private static final long serialVersionUID = 0L; public void actionPerformed(ActionEvent e) { JTabbedPane pane = (JTabbedPane) e.getSource(); if (pane != null && (pane.getUI() instanceof SIPCommTabbedPaneUI)) { SIPCommTabbedPaneUI ui = (SIPCommTabbedPaneUI) pane.getUI(); String command = e.getActionCommand(); if (command != null && command.length() > 0) { int mnemonic = e.getActionCommand().charAt(0); if (mnemonic >= 'a' && mnemonic <= 'z') { mnemonic -= ('a' - 'A'); } Integer index = ui.mnemonicToIndexMap.get(mnemonic); if (index != null && pane.isEnabledAt(index.intValue())) { pane.setSelectedIndex(index.intValue()); } } } } }; private static class ScrollTabsForwardAction extends AbstractAction { private static final long serialVersionUID = 0L; public void actionPerformed(ActionEvent e) { JTabbedPane pane = null; Object src = e.getSource(); if (src instanceof JTabbedPane) { pane = (JTabbedPane) src; } else if (src instanceof ScrollableTabButton) { pane = (JTabbedPane) ((ScrollableTabButton) src).getParent(); } else { return; // shouldn't happen } SIPCommTabbedPaneUI ui = (SIPCommTabbedPaneUI) pane.getUI(); ui.tabScroller.scrollForward(pane.getTabPlacement()); } } private static class ScrollTabsBackwardAction extends AbstractAction { private static final long serialVersionUID = 0L; public void actionPerformed(ActionEvent e) { JTabbedPane pane = null; Object src = e.getSource(); if (src instanceof JTabbedPane) { pane = (JTabbedPane) src; } else if (src instanceof ScrollableTabButton) { pane = (JTabbedPane) ((ScrollableTabButton) src).getParent(); } else { return; // shouldn't happen } SIPCommTabbedPaneUI ui = (SIPCommTabbedPaneUI) pane.getUI(); ui.tabScroller.scrollBackward(pane.getTabPlacement()); } } /** * This inner class is marked "public" due to a compiler bug. This * class should be treated as a "protected" inner class. * Instantiate it only within subclasses of BasicTabbedPaneUI. */ private class TabbedPaneScrollLayout extends TabbedPaneLayout { @Override protected int preferredTabAreaHeight(int tabPlacement, int width) { return calculateMaxTabHeight(tabPlacement); } @Override protected int preferredTabAreaWidth(int tabPlacement, int height) { return calculateMaxTabWidth(tabPlacement); } @Override public void layoutContainer(Container parent) { int tabPlacement = tabPane.getTabPlacement(); int tabCount = tabPane.getTabCount(); Insets insets = tabPane.getInsets(); int selectedIndex = tabPane.getSelectedIndex(); Component visibleComponent = getVisibleComponent(); calculateLayoutInfo(); if (selectedIndex < 0) { if (visibleComponent != null) { // The last tab was removed, so remove the component setVisibleComponent(null); } } else { Component selectedComponent = tabPane.getComponentAt(selectedIndex); boolean shouldChangeFocus = false; // In order to allow programs to use a single component // as the display for multiple tabs, we will not change // the visible component if the currently selected tab // has a null component. This is a bit dicey, as we don't // explicitly state we support this in the spec, but since // programs are now depending on this, we're making it work. // if (selectedComponent != null) { if (selectedComponent != visibleComponent && visibleComponent != null) { if (KeyboardFocusManager.getCurrentKeyboardFocusManager() .getFocusOwner() != null) { shouldChangeFocus = true; } } setVisibleComponent(selectedComponent); } int tx, ty, tw, th; // tab area bounds int cx, cy, cw, ch; // content area bounds Insets contentInsets = getContentBorderInsets(tabPlacement); Rectangle bounds = tabPane.getBounds(); int numChildren = tabPane.getComponentCount(); if (numChildren > 0) { // calculate tab area bounds tw = bounds.width - insets.left - insets.right; th = calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight); tx = insets.left; ty = insets.top; // calculate content area bounds cx = tx + contentInsets.left; cy = ty + th + contentInsets.top; cw = bounds.width - insets.left - insets.right - contentInsets.left - contentInsets.right; ch = bounds.height - insets.top - insets.bottom - th - contentInsets.top - contentInsets.bottom; for (int i = 0; i < numChildren; i++) { Component child = tabPane.getComponent(i); if (child instanceof ScrollableTabViewport) { JViewport viewport = (JViewport) child; Rectangle viewRect = viewport.getViewRect(); int vw = tw; int vh = th; int totalTabWidth = rects[tabCount - 1].x + rects[tabCount - 1].width; if (totalTabWidth > tw) { // Need to allow space for scrollbuttons vw = Math.max(tw - 36, 36); ; if (totalTabWidth - viewRect.x <= vw) { // Scrolled to the end, so ensure the // viewport size is // such that the scroll offset aligns with a // tab vw = totalTabWidth - viewRect.x; } } child.setBounds(tx, ty, vw, vh); } else if (child instanceof ScrollableTabButton) { ScrollableTabButton scrollbutton = (ScrollableTabButton) child; Dimension bsize = scrollbutton.getPreferredSize(); int bx = 0; int by = 0; int bw = bsize.width; int bh = bsize.height; boolean visible = false; int totalTabWidth = rects[tabCount - 1].x + rects[tabCount - 1].width; if (totalTabWidth > tw) { int dir = scrollbutton.scrollsForward() ? EAST : WEST; scrollbutton.setDirection(dir); visible = true; bx = dir == EAST ? bounds.width - insets.left - bsize.width : bounds.width - insets.left - 2 * bsize.width; by = (tabPlacement == TOP ? ty + th - bsize.height : ty); } child.setVisible(visible); if (visible) { child.setBounds(bx, by, bw, bh); } } else { // All content children... child.setBounds(cx, cy, cw, ch); } } if (shouldChangeFocus) { if (!requestMyFocusForVisibleComponent()) { tabPane.requestFocus(); } } } } } @Override protected void calculateTabRects(int tabPlacement, int tabCount) { FontMetrics metrics = getFontMetrics();; Insets tabAreaInsets = getTabAreaInsets(tabPlacement); int i; int x = tabAreaInsets.left - 2; int y = tabAreaInsets.top; int totalWidth = 0; int totalHeight = 0; // // Calculate bounds within which a tab run must fit // maxTabHeight = calculateMaxTabHeight(tabPlacement); runCount = 0; selectedRun = -1; if (tabCount == 0) return; selectedRun = 0; runCount = 1; // Run through tabs and lay them out in a single run Rectangle rect; for (i = 0; i < tabCount; i++) { rect = rects[i]; if (i > 0) { rect.x = rects[i - 1].x + rects[i - 1].width - 1; } else { tabRuns[0] = 0; maxTabWidth = 0; totalHeight += maxTabHeight; rect.x = x; } rect.width = calculateTabWidth(tabPlacement, i, metrics); totalWidth = rect.x + rect.width; maxTabWidth = Math.max(maxTabWidth, rect.width); rect.y = y; rect.height = maxTabHeight /* - 2 */; } // tabPanel.setSize(totalWidth, totalHeight); tabScroller.tabPanel.setPreferredSize(new Dimension(totalWidth, totalHeight)); } } protected class ScrollableTabSupport implements ChangeListener { public ScrollableTabViewport viewport; public ScrollableTabPanel tabPanel; public ScrollableTabButton scrollForwardButton; public ScrollableTabButton scrollBackwardButton; public int leadingTabIndex; private Point tabViewPosition = new Point(0, 0); ScrollableTabSupport(int tabPlacement) { viewport = new ScrollableTabViewport(); tabPanel = new ScrollableTabPanel(); viewport.setView(tabPanel); viewport.addChangeListener(this); scrollForwardButton = createScrollableTabButton(EAST); scrollBackwardButton = createScrollableTabButton(WEST); // scrollForwardButton = new ScrollableTabButton(EAST); // scrollBackwardButton = new ScrollableTabButton(WEST); } public void scrollForward(int tabPlacement) { Dimension viewSize = viewport.getViewSize(); Rectangle viewRect = viewport.getViewRect(); if (tabPlacement == TOP || tabPlacement == BOTTOM) { if (viewRect.width >= viewSize.width - viewRect.x) return; // no room left to scroll } else { // tabPlacement == LEFT || tabPlacement == RIGHT if (viewRect.height >= viewSize.height - viewRect.y) return; } setLeadingTabIndex(tabPlacement, leadingTabIndex + 1); } public void scrollBackward(int tabPlacement) { if (leadingTabIndex == 0) return; // no room left to scroll setLeadingTabIndex(tabPlacement, leadingTabIndex - 1); } public void setLeadingTabIndex(int tabPlacement, int index) { leadingTabIndex = index; Dimension viewSize = viewport.getViewSize(); Rectangle viewRect = viewport.getViewRect(); tabViewPosition.x = leadingTabIndex == 0 ? 0 : rects[leadingTabIndex].x; if ((viewSize.width - tabViewPosition.x) < viewRect.width) { // We've scrolled to the end, so adjust the viewport size // to ensure the view position remains aligned on a tab boundary Dimension extentSize = new Dimension(viewSize.width - tabViewPosition.x, viewRect.height); viewport.setExtentSize(extentSize); } viewport.setViewPosition(tabViewPosition); } public void stateChanged(ChangeEvent e) { JViewport viewport = (JViewport) e.getSource(); int tabPlacement = tabPane.getTabPlacement(); int tabCount = tabPane.getTabCount(); Rectangle vpRect = viewport.getBounds(); Dimension viewSize = viewport.getViewSize(); Rectangle viewRect = viewport.getViewRect(); leadingTabIndex = getClosestTab(viewRect.x, viewRect.y); // If the tab isn't right aligned, adjust it. if (leadingTabIndex + 1 < tabCount) { if (rects[leadingTabIndex].x < viewRect.x) leadingTabIndex++; } Insets contentInsets = getContentBorderInsets(tabPlacement); tabPane.repaint(vpRect.x, vpRect.y + vpRect.height, vpRect.width, contentInsets.top); scrollBackwardButton.setEnabled(viewRect.x > 0); scrollForwardButton.setEnabled(leadingTabIndex < tabCount - 1 && viewSize.width - viewRect.x > viewRect.width); } @Override public String toString() { return new String("viewport.viewSize=" + viewport.getViewSize() + "\n" + "viewport.viewRectangle=" + viewport.getViewRect() + "\n" + "leadingTabIndex=" + leadingTabIndex + "\n" + "tabViewPosition=" + tabViewPosition); } } protected static class ScrollableTabViewport extends JViewport implements UIResource { private static final long serialVersionUID = 0L; public ScrollableTabViewport() { setOpaque(false); setScrollMode(SIMPLE_SCROLL_MODE); } } private class ScrollableTabPanel extends TransparentPanel implements UIResource { /** * Serial version UID. */ private static final long serialVersionUID = 0L; public ScrollableTabPanel() { setLayout(null); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); SIPCommTabbedPaneUI.this.paintTabArea(g, tabPane.getTabPlacement(), tabPane.getSelectedIndex()); } } protected static class ScrollableTabButton extends BasicArrowButton implements UIResource, SwingConstants { private static final long serialVersionUID = 0L; public ScrollableTabButton(int direction) { super(direction, UIManager.getColor("TabbedPane.selected"), UIManager.getColor("TabbedPane.shadow"), UIManager .getColor("TabbedPane.darkShadow"), UIManager .getColor("TabbedPane.highlight")); } public boolean scrollsForward() { return direction == EAST || direction == SOUTH; } } /** * This inner class is marked "public" due to a compiler bug. This * class should be treated as a "protected" inner class. * Instantiate it only within subclasses of BasicTabbedPaneUI. */ private class TabSelectionHandler implements ChangeListener { public void stateChanged(ChangeEvent e) { JTabbedPane tabPane = (JTabbedPane) e.getSource(); tabPane.revalidate(); tabPane.repaint(); if (tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT) { int index = tabPane.getSelectedIndex(); if (index < rects.length && index != -1) tabScroller.tabPanel.scrollRectToVisible(rects[index]); } } } /** * This inner class is marked "public" due to a compiler bug. This * class should be treated as a "protected" inner class. * Instantiate it only within subclasses of BasicTabbedPaneUI. */ /* * GES 2/3/99: The container listener code was added to support HTML * rendering of tab titles. * * Ideally, we would be able to listen for property changes when a tab is * added or its text modified. At the moment there are no such events * because the Beans spec doesn't allow 'indexed' property changes (i.e. tab * 2's text changed from A to B). * * In order to get around this, we listen for tabs to be added or removed by * listening for the container events. we then queue up a runnable (so the * component has a chance to complete the add) which checks the tab title of * the new component to see if it requires HTML rendering. * * The Views (one per tab title requiring HTML rendering) are stored in the * htmlViews Vector, which is only allocated after the first time we run * into an HTML tab. Note that this vector is kept in step with the number * of pages, and nulls are added for those pages whose tab title do not * require HTML rendering. * * This makes it easy for the paint and layout code to tell whether to * invoke the HTML engine without having to check the string during * time-sensitive operations. * * When we have added a way to listen for tab additions and changes to tab * text, this code should be removed and replaced by something which uses * that. */ private class ContainerHandler implements ContainerListener { public void componentAdded(ContainerEvent e) { JTabbedPane tp = (JTabbedPane) e.getContainer(); Component child = e.getChild(); if (child instanceof UIResource) return; int index = tp.indexOfComponent(child); String title = tp.getTitleAt(index); boolean isHTML = BasicHTML.isHTMLString(title); if (isHTML) { if (htmlViews == null) { // Initialize vector htmlViews = createHTMLVector(); } else { // Vector already exists View v = BasicHTML.createHTMLView(tp, title); htmlViews.insertElementAt(v, index); } } else { // Not HTML if (htmlViews != null) { // Add placeholder htmlViews.insertElementAt(null, index); } // else nada! } } public void componentRemoved(ContainerEvent e) { JTabbedPane tp = (JTabbedPane) e.getContainer(); Component child = e.getChild(); if (child instanceof UIResource) return; // NOTE 4/15/2002 (joutwate): // This fix is implemented using client properties since there is // currently no IndexPropertyChangeEvent. Once // IndexPropertyChangeEvents have been added this code should be // modified to use it. Integer indexObj = (Integer) tp .getClientProperty("__index_to_remove__"); if (indexObj != null) { int index = indexObj.intValue(); if (htmlViews != null && htmlViews.size() >= index) { htmlViews.removeElementAt(index); } } } } private Vector<View> createHTMLVector() { Vector<View> htmlViews = new Vector<View>(); int count = tabPane.getTabCount(); if (count > 0) { for (int i = 0; i < count; i++) { String title = tabPane.getTitleAt(i); if (BasicHTML.isHTMLString(title)) { htmlViews.addElement(BasicHTML.createHTMLView(tabPane, title)); } else { htmlViews.addElement(null); } } } return htmlViews; } private class MyMouseHandler extends MouseHandler { @Override public void mousePressed(MouseEvent e) { if (closeIndexStatus == OVER) { closeIndexStatus = PRESSED; tabScroller.tabPanel.repaint(); } else if (maxIndexStatus == OVER) { maxIndexStatus = PRESSED; tabScroller.tabPanel.repaint(); } else { super.mousePressed(e); } } @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() > 1 && overTabIndex != -1) { ((SIPCommTabbedPane) tabPane).fireDoubleClickTabEvent(e, overTabIndex); } } @Override public void mouseReleased(MouseEvent e) { updateOverTab(e.getX(), e.getY()); if (overTabIndex == -1) { if (e.isPopupTrigger()) ((SIPCommTabbedPane) tabPane).firePopupOutsideTabEvent(e); return; } if (isOneActionButtonEnabled() && e.isPopupTrigger()) { super.mousePressed(e); closeIndexStatus = INACTIVE; // Prevent undesired action when maxIndexStatus = INACTIVE; // right-clicking on icons actionPopupMenu.show(tabScroller.tabPanel, e.getX(), e.getY()); return; } if (closeIndexStatus == PRESSED) { closeIndexStatus = OVER; tabScroller.tabPanel.repaint(); ((SIPCommTabbedPane) tabPane) .fireCloseTabEvent(e, overTabIndex); return; } if (maxIndexStatus == PRESSED) { maxIndexStatus = OVER; tabScroller.tabPanel.repaint(); ((SIPCommTabbedPane) tabPane).fireMaxTabEvent(e, overTabIndex); return; } // Allow tabs closing with mouse middle button if (e.getButton() == MouseEvent.BUTTON2) ((SIPCommTabbedPane) tabPane).fireCloseTabEvent(e, overTabIndex); } @Override public void mouseExited(MouseEvent e) { if (!mousePressed) { overTabIndex = -1; tabScroller.tabPanel.repaint(); } } } private class MyMouseMotionListener implements MouseMotionListener { public void mouseMoved(MouseEvent e) { if (actionPopupMenu.isVisible()) return; // No updates when popup is visible mousePressed = false; setTabIcons(e.getX(), e.getY()); } public void mouseDragged(MouseEvent e) { if (actionPopupMenu.isVisible()) return; // No updates when popup is visible mousePressed = true; setTabIcons(e.getX(), e.getY()); } } /** * We don't want to have a content border. */ @Override protected void paintContentBorder( Graphics g, int tabPlacement, int selectedIndex) {} /** * Reloads close icon. */ public void loadSkin() { closeImgI = DesktopUtilActivator.getImage(CLOSE_TAB_ICON); maxImgI = new BufferedImage(BUTTONSIZE, BUTTONSIZE, BufferedImage.TYPE_4BYTE_ABGR); } }