1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.xnap.commons.gui;
22
23 import java.awt.Component;
24 import java.awt.Graphics;
25 import java.awt.Insets;
26 import java.awt.Point;
27 import java.awt.Rectangle;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ComponentAdapter;
30 import java.awt.event.ComponentEvent;
31 import java.awt.event.MouseAdapter;
32 import java.awt.event.MouseEvent;
33 import java.awt.event.MouseMotionListener;
34 import java.lang.reflect.Method;
35
36 import javax.swing.AbstractButton;
37 import javax.swing.Action;
38 import javax.swing.Box;
39 import javax.swing.BoxLayout;
40 import javax.swing.Icon;
41 import javax.swing.JLabel;
42 import javax.swing.JLayeredPane;
43 import javax.swing.JRootPane;
44 import javax.swing.JTabbedPane;
45 import javax.swing.SwingConstants;
46 import javax.swing.SwingUtilities;
47 import javax.swing.UIManager;
48 import javax.swing.event.ChangeEvent;
49 import javax.swing.event.ChangeListener;
50
51 import org.xnap.commons.gui.action.AbstractXNapAction;
52 import org.xnap.commons.gui.util.IconHelper;
53
54 /***
55 * Provides a <code>JTabbedPane</code> with close buttons in the tab titles.
56 * The buttons of the currently active tab will be visible and the button
57 * of the tab the mouse if hovered over.
58 *
59 * <p>
60 * If a button is clicked the tab is removed from the pane unless a CloseListener
61 * has been set.
62 *
63 * <p>Note: If a button is displayed on a non active tab and the mouse is
64 * moved very fast out of the tabbed pane, the button may still be visible.
65 *
66 * @author Felix Berger
67 * @author Steffen Pingel
68 */
69 @SuppressWarnings("serial")
70 public class CloseableTabbedPane extends JTabbedPane {
71
72 private final static int ICON_TEXT_GAP = 8;
73 private MouseHandler mouseHandler = new MouseHandler();
74 private SelectionHandler selHandler = new SelectionHandler();
75 private ComponentHandler compHandler = new ComponentHandler();
76 private CloseListener closeListener = null;
77 private boolean useScrollHack;
78 private Method getTabComponentAtMethod;
79 private Method setTabComponentAtMethod;
80 private boolean java6 = false;
81
82 /***
83 * Creates a tabbed pane with close buttons and a
84 * <code>WRAP_TAB_LAYOUT</code>
85 */
86 public CloseableTabbedPane()
87 {
88 try {
89 getTabComponentAtMethod = getClass().getMethod("getTabComponentAt", int.class);
90 setTabComponentAtMethod = getClass().getMethod("setTabComponentAt", int.class, Component.class);
91 java6 = true;
92 } catch (Throwable t) {
93 }
94
95 setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
96 addMouseMotionListener(mouseHandler);
97 addMouseListener(mouseHandler);
98 getModel().addChangeListener(selHandler);
99 addComponentListener(compHandler);
100 }
101
102 /***
103 * Sets the listener that is notified of user initiated close requests
104 * for single tabs.
105 * @param listener can be <code>null</code>, then the component
106 * is simply removed
107 */
108 public void setCloseListener(CloseListener listener)
109 {
110 closeListener = listener;
111 }
112
113 /***
114 * Adds a <code>compnent</code> with a tab <code>title</code> and
115 * an <code>icon</code> to the tabbed pane.
116 * <p>
117 * The icon is displayed on the left side of the title and does not affect
118 * the icon of the closing button for this tab.
119 * @param title the title of the tab, can be <code>null</code>
120 * @param component
121 * @param icon can be <code>null</code>
122 * @param closeable whether or not a close button should be displayed
123 * for this tabbed pane.
124 */
125 public void addTab(String title, Component component, Icon icon,
126 boolean closeable)
127 {
128 if (closeable) {
129 if (java6) {
130 super.addTab(null, component);
131 TabTitleComponent tabTitle = new TabTitleComponent(title, component, icon);
132 try {
133 setTabComponentAtMethod.invoke(this, this.getTabCount() - 1, tabTitle);
134 } catch (Throwable t) {
135 throw new RuntimeException(t);
136 }
137 }
138 else {
139 super.addTab(null, new TabTitleIcon(title, component, icon), component);
140 }
141 }
142 else {
143 super.addTab(title, icon, component);
144 }
145 setSelectedComponent(component);
146 }
147
148 /***
149 * Convenience wrapper for {@link #addTab(String, Component, Icon, boolean)
150 * addTab(String, Component, Icon, true)}.
151 */
152 public void addTab(String title, Component component, Icon icon)
153 {
154 addTab(title, component, icon, true);
155 }
156
157 /***
158 * Convenience wrapper for {@link #addTab(String, Component, Icon, boolean)
159 * addTab(String, Component, null, true)}.
160 */
161 @Override
162 public void addTab(String title, Component component)
163 {
164 addTab(title, component, null, true);
165 }
166
167 @Override
168 public void setTitleAt(int index, String newTitle)
169 {
170 TabTitle titleComponent = getTabTitleComponentAt(index);
171 if (titleComponent != null) {
172 titleComponent.setTitle(newTitle);
173 }
174 else {
175 super.setTitleAt(index, newTitle);
176 }
177 }
178
179 private TabTitle getTabTitleComponentAt(int index) {
180 if (java6) {
181 try {
182 Object tabTitle = getTabComponentAtMethod.invoke(this, index);
183 if (tabTitle instanceof TabTitle) {
184 return (TabTitle) tabTitle;
185 }
186 } catch (Throwable t) {
187 throw new RuntimeException(t);
188 }
189 } else {
190 Icon icon = getIconAt(index);
191 if (icon instanceof TabTitle) {
192 return (TabTitle)icon;
193 }
194 }
195 return null;
196 }
197
198 /***
199 * This method does not work properly for tabs that have a close
200 * button.
201 * <p>
202 * Keep that information somewhere else if you need it.
203 * @return the empty string for tabs that have a close button,
204 * the title of the tab otherwise
205 */
206 @Override
207 public String getTitleAt(int index)
208 {
209 return super.getTitleAt(index);
210 }
211
212 /***
213 * Sets the tab layout policy. Currently <code>WRAP_TAB_LAYOUT</code> is
214 * strongly recommended.
215 *
216 * <p>
217 * Explanation: <code>SCROLL_TAB_LAYOUT</code> uses a private view
218 * translation for the scrolling of the tabs in
219 * <code>BasicTabbedPaneUI</code>. The <code>EventIcon.paint()</code>
220 * method receives coordinates from the ui view port and since it is not
221 * possible to translate coordinates between the tabbed pane and the view
222 * port the closing button can not be placed at the correct location.
223 *
224 * <p>
225 * However there is a crude work around:
226 * <code>TabbedPaneUI.getTabBounds()</code> returns tabbed pane
227 * coordinates whereas <code>TabTitleIcon.paint()</code> receives view
228 * port coordinates. When <code>TabTitleIcon.paint()</code> is invoked it
229 * can finds out its tab bounds in tabbed pane coordinates and use those
230 * to set the button's bounds. The location can only be somewhat estimated
231 * as it depends on the tab style of the look&feel.
232 *
233 * @param tabLayoutPolicy
234 * must be <code>JTabbedPane.WRAP_TAB_LAYOUT</code>
235 * @see javax.swing.JTabbedPane#setTabLayoutPolicy(int)
236 */
237 @Override
238 public void setTabLayoutPolicy(int tabLayoutPolicy)
239 {
240 useScrollHack = (tabLayoutPolicy == SCROLL_TAB_LAYOUT);
241 super.setTabLayoutPolicy(tabLayoutPolicy);
242 }
243
244 @Override
245 public void updateUI()
246 {
247 super.updateUI();
248 int tabCount = getTabCount();
249 for (int i = 0; i < tabCount; i++) {
250 Icon icon = getIconAt(i);
251 if (icon instanceof TabTitle) {
252 ((TabTitle)icon).updateUI();
253 }
254 }
255 }
256
257 private void setButtonVisible(int index, boolean visible)
258 {
259 TabTitle tabTitle = getTabTitleComponentAt(index);
260 if (tabTitle != null) {
261 tabTitle.setButtonVisible(visible);
262 }
263 }
264
265 @Override
266 public void removeTabAt(int index) {
267 TabTitle tabTitle = getTabTitleComponentAt(index);
268 super.removeTabAt(index);
269 if (tabTitle != null) {
270 tabTitle.disable();
271 }
272 if (mouseHandler.visibleIndex == index) {
273 mouseHandler.visibleIndex = -1;
274 }
275 if (selHandler.oldIndex == index) {
276 selHandler.oldIndex = -1;
277 }
278 int sel = getSelectedIndex();
279 if (sel != -1) {
280 setButtonVisible(sel, true);
281 }
282 }
283
284 public static interface CloseListener
285 {
286 /***
287 * Called when the user clicked the close button of the tab containing
288 * <code>component</code>.
289 * @param component that should be removed
290 */
291 void closeRequested(Component component);
292 }
293
294 public interface TabTitle {
295
296 public abstract void updateUI();
297
298 public abstract void setTitle(String newTitle);
299
300 public abstract String getTitle();
301
302 public abstract void setButtonVisible(boolean visible);
303
304 public abstract void disable();
305
306 }
307
308 private class DefaultCloseAction extends AbstractXNapAction
309 {
310
311 Component comp;
312
313 public DefaultCloseAction(Component comp)
314 {
315 this.comp = comp;
316 putValue(AbstractXNapAction.ICON_FILENAME, "remove.png");
317 }
318
319 public void actionPerformed(ActionEvent e) {
320 if (closeListener != null) {
321 closeListener.closeRequested(comp);
322 }
323 else {
324 remove(comp);
325 }
326 }
327
328 }
329
330 protected class TabTitleComponent extends Box implements TabTitle {
331
332 private JLabel titleLabel;
333 private TabTitleButton closeButton;
334 private Component placeholder;
335
336 public TabTitleComponent(String title, Component component,
337 Icon icon) {
338 super(BoxLayout.X_AXIS);
339
340 titleLabel = new JLabel(title);
341 titleLabel.setIcon(icon);
342 titleLabel.setIconTextGap(ICON_TEXT_GAP);
343 titleLabel.setHorizontalTextPosition(SwingConstants.RIGHT);
344 add(titleLabel);
345
346 add(Box.createHorizontalStrut(ICON_TEXT_GAP));
347
348 closeButton = new TabTitleButton(new DefaultCloseAction(component));
349 add(closeButton);
350
351
352
353 placeholder = Box.createRigidArea(closeButton.getPreferredSize());
354 }
355
356 public void disable() {
357 }
358
359 public String getTitle() {
360 return titleLabel.getText();
361 }
362
363 public void setTitle(String newTitle) {
364 titleLabel.setText(newTitle);
365 }
366
367 public void setButtonVisible(boolean visible) {
368 closeButton.setVisible(visible);
369 if (!visible) {
370 add(placeholder);
371 } else {
372 remove(placeholder);
373 }
374 }
375
376 }
377
378 /***
379 * Provides an Icon that can displays a text that can have an icon to
380 * its left and an <code>EventIcon</code> to its right.
381 */
382 protected class TabTitleIcon implements Icon, TabTitle {
383
384 private Icon leftIcon;
385 private EventIcon closeButtonIcon;
386 private String title;
387 private int height = 10;
388
389 public TabTitleIcon(String title, Component comp, Icon leftIcon)
390 {
391 this.title = title;
392 this.leftIcon = leftIcon;
393
394 this.closeButtonIcon = new EventIcon(new TabTitleButton(new DefaultCloseAction(comp)));
395
396 height = Math.max(this.closeButtonIcon.getIconHeight(), leftIcon != null ?
397 leftIcon.getIconHeight() : 0);
398 }
399
400
401
402
403 public void updateUI() {
404 closeButtonIcon.updateUI();
405 }
406
407 public TabTitleIcon(String title, Component comp)
408 {
409 this(title, comp, null);
410 }
411
412
413
414
415 public void setTitle(String newTitle)
416 {
417 title = newTitle;
418 }
419
420
421
422
423 public String getTitle()
424 {
425 return title;
426 }
427
428
429
430
431 public int getIconHeight()
432 {
433 return height;
434 }
435
436 public int getIconWidth()
437 {
438 int textWidth= SwingUtilities.computeStringWidth
439 (CloseableTabbedPane.this.getFontMetrics(CloseableTabbedPane.this.getFont()),
440 title);
441 if (leftIcon != null) {
442 return leftIcon.getIconWidth() + closeButtonIcon.getIconWidth()
443 + textWidth + 2 * ICON_TEXT_GAP;
444 }
445 else {
446 return textWidth + ICON_TEXT_GAP + closeButtonIcon.getIconWidth();
447 }
448 }
449
450 /***
451 * Overwrites paintIcon to get hold of the coordinates of the icon.
452 */
453 public void paintIcon(Component c, Graphics g, int x, int y)
454 {
455 if (leftIcon != null) {
456 leftIcon.paintIcon(c, g, x, y + 1);
457 drawTitleAndRightIcon(c, g, x, y, leftIcon.getIconWidth() + ICON_TEXT_GAP);
458 }
459 else {
460 drawTitleAndRightIcon(c, g, x, y, 0);
461 }
462 }
463
464 protected Rectangle computeTextRect(Graphics g, int x, int y) {
465
466 Rectangle rect = new Rectangle(x, y, getIconWidth(),
467 getIconHeight());
468 Rectangle iconRect = new Rectangle();
469 Rectangle textRect = new Rectangle();
470 SwingUtilities.layoutCompoundLabel
471 (CloseableTabbedPane.this, g.getFontMetrics(),
472 title, null, SwingUtilities.CENTER,
473 SwingUtilities.CENTER,
474 SwingUtilities.CENTER,
475 SwingUtilities.TRAILING,
476 rect, iconRect, textRect,
477 UIManager.getInt("TabbedPane.textIconGap"));
478 return textRect;
479 }
480
481 protected void drawTitleAndRightIcon(Component c, Graphics g, int x, int y, int offset)
482 {
483 Rectangle textRect = computeTextRect(g, x, y);
484
485 int index = CloseableTabbedPane.this.indexOfTab(this);
486 g.setColor((index != -1)
487 ? CloseableTabbedPane.this.getForegroundAt(index)
488 : CloseableTabbedPane.this.getForegroundAt(index));
489 g.setFont(CloseableTabbedPane.this.getFont());
490 g.drawString(title, x + offset,
491 textRect.y + g.getFontMetrics().getAscent());
492
493 if (useScrollHack) {
494 if (index != -1) {
495 Rectangle bounds = getUI().getTabBounds(CloseableTabbedPane.this, index);
496 Insets tabInsets = UIManager.getInsets("TabbedPane.tabInsets");
497 closeButtonIcon.paintIcon
498 (c, g, bounds.x + tabInsets.left + getIconWidth() + 3 - closeButtonIcon.getIconWidth(), bounds.y + tabInsets.top + 1);
499 }
500 else {
501
502 closeButtonIcon.paintIcon
503 (c, g, x + getIconWidth() - closeButtonIcon.getIconWidth(), y + 1);
504 }
505 }
506 else {
507 closeButtonIcon.paintIcon
508 (c, g, x + getIconWidth() - closeButtonIcon.getIconWidth(), y + 1);
509 }
510 }
511
512 public void setButtonVisible(boolean b) {
513 closeButtonIcon.setVisible(b);
514 }
515
516 public void disable() {
517 closeButtonIcon.disable();
518 }
519
520 }
521
522 /***
523 * Acts as a proxy class for the closing icon.
524 */
525 private class EventIcon implements Icon {
526
527 AbstractButton button;
528 JLayeredPane pane;
529
530 public EventIcon(AbstractButton button)
531 {
532 this.button = button;
533 JRootPane rootPane = SwingUtilities.getRootPane(CloseableTabbedPane.this);
534 if (rootPane != null) {
535 pane = rootPane.getLayeredPane();
536 pane.add(button, JLayeredPane.PALETTE_LAYER);
537 }
538 }
539
540 public void updateUI() {
541 button.updateUI();
542 }
543
544 public void disable() {
545 button.setVisible(false);
546 button.setEnabled(false);
547 pane.remove(button);
548 }
549
550 public void setVisible(boolean b) {
551 button.setVisible(b);
552 }
553
554 public int getIconHeight()
555 {
556 return button.getPreferredSize().height;
557 }
558
559 public int getIconWidth()
560 {
561 return button.getPreferredSize().width;
562 }
563
564 /***
565 * Repositions the button.
566 */
567 public void paintIcon(Component c, Graphics g, int x, int y)
568 {
569 if (pane == null) {
570 JRootPane rootPane = SwingUtilities.getRootPane(CloseableTabbedPane.this);
571 pane = rootPane.getLayeredPane();
572 pane.add(button, JLayeredPane.PALETTE_LAYER);
573 }
574
575 Point p = SwingUtilities.convertPoint(c, x, y, pane);
576 button.setBounds(p.x, p.y, getIconHeight(), getIconWidth());
577 }
578 }
579
580 private class TabTitleButton extends ToolBarButton
581 {
582
583 public TabTitleButton(Action action)
584 {
585 super(action);
586 String iconName = (String)action.getValue(AbstractXNapAction.ICON_FILENAME);
587 setIcon(IconHelper.getTabTitleIcon(iconName));
588 setMargin(new Insets(0, 0, 0, 0));
589 }
590 }
591
592 private class MouseHandler extends MouseAdapter implements MouseMotionListener
593 {
594
595 int visibleIndex = -1;
596
597 @Override
598 public void mouseEntered(MouseEvent event)
599 {
600 updateButton(event);
601 }
602
603 @Override
604 public void mouseExited(MouseEvent event)
605 {
606
607
608 }
609
610 public void mouseMoved(MouseEvent event) {
611 updateButton(event);
612 }
613
614 public void mouseDragged(MouseEvent event)
615 {
616 updateButton(event);
617 }
618
619 private void updateButton(MouseEvent event)
620 {
621 int index = getUI().tabForCoordinate(CloseableTabbedPane.this, event.getX(), event.getY());
622 if (index != -1 && index != getSelectedIndex()) {
623 if (visibleIndex == index) {
624 return;
625 }
626 setButtonVisible(index, true);
627 if (visibleIndex != index && visibleIndex != -1 && visibleIndex != getSelectedIndex()) {
628 setButtonVisible(visibleIndex, false);
629 }
630 visibleIndex = index;
631 } else if (visibleIndex != -1 && visibleIndex != getSelectedIndex() && visibleIndex < getTabCount()) {
632 setButtonVisible(visibleIndex, false);
633 visibleIndex = -1;
634 }
635 }
636 }
637
638 private class SelectionHandler implements ChangeListener
639 {
640 int oldIndex = -1;
641
642 /***
643 * Invoked when a tab is selected.
644 */
645 public void stateChanged(ChangeEvent e) {
646 if (oldIndex != -1 && oldIndex < CloseableTabbedPane.this.getTabCount()) {
647 CloseableTabbedPane.this.setButtonVisible(oldIndex, false);
648 }
649 oldIndex = getSelectedIndex();
650 if (oldIndex != -1) {
651 CloseableTabbedPane.this.setButtonVisible(oldIndex, true);
652 }
653 }
654
655 }
656
657 private class ComponentHandler extends ComponentAdapter
658 {
659
660 @Override
661 public void componentHidden(ComponentEvent e) {
662 int sel = getSelectedIndex();
663 if (sel != -1) {
664 setButtonVisible(sel, false);
665 }
666 }
667
668 @Override
669 public void componentShown(ComponentEvent e) {
670 int sel = getSelectedIndex();
671 if (sel != -1) {
672 setButtonVisible(sel, true);
673 }
674 }
675
676 }
677 }