View Javadoc
1   package com.jsql.view.swing.tab.dnd;
2   
3   import com.jsql.util.LogLevelUtil;
4   import com.jsql.view.swing.action.ActionCloseTabResult;
5   import org.apache.logging.log4j.LogManager;
6   import org.apache.logging.log4j.Logger;
7   
8   import javax.swing.*;
9   import java.awt.*;
10  import java.awt.dnd.DragSource;
11  import java.awt.event.MouseAdapter;
12  import java.awt.event.MouseEvent;
13  import java.beans.PropertyChangeEvent;
14  import java.beans.PropertyChangeListener;
15  import java.util.Objects;
16  import java.util.Optional;
17  
18  public class DnDTabbedPane extends JTabbedPane {
19  
20      private static final Logger LOGGER = LogManager.getRootLogger();
21  
22      private static final int SCROLL_SIZE = 20;  // Test
23      private static final int BUTTON_SIZE = 30;  // 30 is magic number of scroll button size
24      private static final int LINE_WIDTH = 3;
25      private static final Rectangle RECT_BACKWARD = new Rectangle();
26      private static final Rectangle RECT_FORWARD = new Rectangle();
27      protected static final Rectangle RECT_LINE = new Rectangle();
28      protected int dragTabIndex = -1;
29      private transient DnDDropLocation dropLocation;
30  
31      public static final class DnDDropLocation extends TransferHandler.DropLocation {
32          
33          private final int index;
34          private boolean dropable = true;
35          
36          private DnDDropLocation(Point p, int index) {
37              super(p);
38              this.index = index;
39          }
40          
41          public int getIndex() {
42              return this.index;
43          }
44          
45          public void setDroppable(boolean flag) {
46              this.dropable = flag;
47          }
48          
49          public boolean isDroppable() {
50              return this.dropable;
51          }
52      }
53      
54      private void clickArrowButton(String actionKey) {
55          JButton scrollForwardButton = null;
56          JButton scrollBackwardButton = null;
57          
58          for (Component c: this.getComponents()) {
59              if (c instanceof JButton) {
60                  if (scrollForwardButton == null) {
61                      scrollForwardButton = (JButton) c;
62                  } else if (scrollBackwardButton == null) {
63                      scrollBackwardButton = (JButton) c;
64                  }
65              }
66          }
67          
68          JButton button = "scrollTabsForwardAction".equals(actionKey) ? scrollForwardButton : scrollBackwardButton;
69          Optional.ofNullable(button)
70              .filter(JButton::isEnabled)
71              .ifPresent(JButton::doClick);
72      }
73      
74      public void autoScrollTest(Point pt) {
75          Rectangle r = this.getTabAreaBounds();
76          
77          if (DnDTabbedPane.isTopBottomTabPlacement(this.getTabPlacement())) {
78              DnDTabbedPane.RECT_BACKWARD.setBounds(r.x, r.y, DnDTabbedPane.SCROLL_SIZE, r.height);
79              DnDTabbedPane.RECT_FORWARD.setBounds(r.x + r.width - DnDTabbedPane.SCROLL_SIZE - DnDTabbedPane.BUTTON_SIZE, r.y, DnDTabbedPane.SCROLL_SIZE + DnDTabbedPane.BUTTON_SIZE, r.height);
80          } else {
81              DnDTabbedPane.RECT_BACKWARD.setBounds(r.x, r.y, r.width, DnDTabbedPane.SCROLL_SIZE);
82              DnDTabbedPane.RECT_FORWARD.setBounds(r.x, r.y + r.height - DnDTabbedPane.SCROLL_SIZE - DnDTabbedPane.BUTTON_SIZE, r.width, DnDTabbedPane.SCROLL_SIZE + DnDTabbedPane.BUTTON_SIZE);
83          }
84          
85          if (DnDTabbedPane.RECT_BACKWARD.contains(pt)) {
86              this.clickArrowButton("scrollTabsBackwardAction");
87          } else if (DnDTabbedPane.RECT_FORWARD.contains(pt)) {
88              this.clickArrowButton("scrollTabsForwardAction");
89          }
90      }
91      
92      protected DnDTabbedPane() {
93          super();
94          
95          var h = new Handler();
96          this.addMouseListener(h);
97          this.addMouseMotionListener(h);
98          this.addPropertyChangeListener(h);
99      }
100     
101     public DnDDropLocation dropLocationForPointDnD(Point p) {
102         for (var i = 0; i < this.getTabCount(); i++) {
103             if (this.getBoundsAt(i).contains(p)) {
104                 return new DnDDropLocation(p, i);
105             }
106         }
107         
108         if (this.getTabAreaBounds().contains(p)) {
109             return new DnDDropLocation(p, this.getTabCount());
110         }
111         
112         return new DnDDropLocation(p, -1);
113     }
114     
115     public void setDropLocation(TransferHandler.DropLocation location, boolean forDrop) {
116         DnDDropLocation old = this.dropLocation;
117         
118         if (Objects.isNull(location) || !forDrop) {
119             this.dropLocation = new DnDDropLocation(new Point(), -1);
120         } else if (location instanceof DnDDropLocation) {
121             this.dropLocation = (DnDDropLocation) location;
122         }
123         
124         this.firePropertyChange("dropLocation", old, this.dropLocation);
125     }
126     
127     public void exportTab(int dragIndex, JTabbedPane target, int targetIndex) {
128         var cmp = this.getComponentAt(dragIndex);
129         var tab = this.getTabComponentAt(dragIndex);
130         String title = this.getTitleAt(dragIndex);
131         var icon = this.getIconAt(dragIndex);
132         String tip = this.getToolTipTextAt(dragIndex);
133         boolean isEnabled = this.isEnabledAt(dragIndex);
134         
135         this.remove(dragIndex);
136         target.insertTab(title, icon, cmp, tip, targetIndex);
137         target.setEnabledAt(targetIndex, isEnabled);
138 
139         target.setTabComponentAt(targetIndex, tab);
140         target.setSelectedIndex(targetIndex);
141         
142         if (tab instanceof JComponent) {
143             ((JComponent) tab).scrollRectToVisible(tab.getBounds());
144         }
145     }
146     
147     public void convertTab(int prev, int next) {
148         var cmp = this.getComponentAt(prev);
149         var tab = this.getTabComponentAt(prev);
150         String title = this.getTitleAt(prev);
151         var icon = this.getIconAt(prev);
152         String tip = this.getToolTipTextAt(prev);
153         boolean isEnabled = this.isEnabledAt(prev);
154         int tgtindex = prev > next ? next : next - 1;
155         
156         this.remove(prev);
157         this.insertTab(title, icon, cmp, tip, tgtindex);
158         this.setEnabledAt(tgtindex, isEnabled);
159         
160         // When you drag'n'drop a disabled tab, it finishes enabled and selected.
161         // pointed out by dlorde
162         if (isEnabled) {
163             this.setSelectedIndex(tgtindex);
164         }
165         
166         // I have a component in all tabs (jlabel with an X to close the tab) and when I move a tab the component disappear.
167         // pointed out by Daniel Dario Morales Salas
168         this.setTabComponentAt(tgtindex, tab);
169     }
170     
171     public Optional<Rectangle> getDropLineRect() {
172         int index = Optional.ofNullable(this.getDropLocation())
173             .filter(DnDDropLocation::isDroppable)
174             .map(DnDDropLocation::getIndex)
175             .orElse(-1);
176         
177         if (index < 0) {
178             DnDTabbedPane.RECT_LINE.setBounds(0, 0, 0, 0);
179             return Optional.empty();
180         }
181         
182         int a = Math.min(index, 1);
183         Rectangle r = this.getBoundsAt(a * (index - 1));
184         
185         if (DnDTabbedPane.isTopBottomTabPlacement(this.getTabPlacement())) {
186             DnDTabbedPane.RECT_LINE.setBounds(r.x - DnDTabbedPane.LINE_WIDTH / 2 + r.width * a, r.y, DnDTabbedPane.LINE_WIDTH, r.height);
187         } else {
188             DnDTabbedPane.RECT_LINE.setBounds(r.x, r.y - DnDTabbedPane.LINE_WIDTH / 2 + r.height * a, r.width, DnDTabbedPane.LINE_WIDTH);
189         }
190         
191         return Optional.of(DnDTabbedPane.RECT_LINE);
192     }
193     
194     public Rectangle getTabAreaBounds() {
195         Rectangle tabbedRect = this.getBounds();
196         int xx = tabbedRect.x;
197         int yy = tabbedRect.y;
198         
199         Rectangle compRect = Optional.ofNullable(this.getSelectedComponent())
200             .map(Component::getBounds)
201             .orElseGet(Rectangle::new);
202 
203         int tabPlacement = this.getTabPlacement();
204         
205         if (DnDTabbedPane.isTopBottomTabPlacement(tabPlacement)) {
206             tabbedRect.height = tabbedRect.height - compRect.height;
207             if (tabPlacement == SwingConstants.BOTTOM) {
208                 tabbedRect.y += compRect.y + compRect.height;
209             }
210         } else {
211             tabbedRect.width = tabbedRect.width - compRect.width;
212             if (tabPlacement == SwingConstants.RIGHT) {
213                 tabbedRect.x += compRect.x + compRect.width;
214             }
215         }
216         
217         tabbedRect.translate(-xx, -yy);
218         return tabbedRect;
219     }
220 
221     public static boolean isTopBottomTabPlacement(int tabPlacement) {
222         return tabPlacement == SwingConstants.TOP || tabPlacement == SwingConstants.BOTTOM;
223     }
224 
225     private class Handler extends MouseAdapter implements PropertyChangeListener { // , BeforeDrag
226         
227         private Point startPt;
228         private final int gestureMotionThreshold = DragSource.getDragThreshold();
229 
230         private void repaintDropLocation() {
231             Component c = DnDTabbedPane.this.getRootPane().getGlassPane();
232             if (c instanceof GhostGlassPane) {
233                 GhostGlassPane glassPane = (GhostGlassPane) c;
234                 glassPane.setTargetTabbedPane(DnDTabbedPane.this);
235                 glassPane.repaint();
236             }
237         }
238         
239         // PropertyChangeListener
240         @Override
241         public void propertyChange(PropertyChangeEvent e) {
242             String propertyName = e.getPropertyName();
243             if ("dropLocation".equals(propertyName)) {
244                 this.repaintDropLocation();
245             }
246         }
247         
248         // MouseListener
249         @Override
250         public void mousePressed(MouseEvent e) {
251             DnDTabbedPane src = (DnDTabbedPane) e.getComponent();
252             boolean isOnlyOneTab = src.getTabCount() <= 1;
253             if (isOnlyOneTab) {
254                 this.startPt = null;
255                 return;
256             }
257             
258             var tabPt = e.getPoint();
259             int idx;
260             // Fix #95782: IllegalArgumentException on indexAtLocation()
261             try {
262                 idx = src.indexAtLocation(tabPt.x, tabPt.y);
263             } catch (IllegalArgumentException err) {
264                 LOGGER.log(LogLevelUtil.CONSOLE_JAVA, err);
265                 return;
266             }
267             
268             // disabled tab, null component problem.
269             // pointed out by daryl. NullPointerException: i.e. addTab("Tab", null)
270             boolean flag = idx < 0 || !src.isEnabledAt(idx) || Objects.isNull(src.getComponentAt(idx));
271             
272             this.startPt = flag ? null : tabPt;
273         }
274         
275         @Override
276         public void mouseDragged(MouseEvent e) {
277             var tabPt = e.getPoint();
278             if (Objects.nonNull(this.startPt) && this.startPt.distance(tabPt) > this.gestureMotionThreshold) {
279                 DnDTabbedPane src = (DnDTabbedPane) e.getComponent();
280                 var th = src.getTransferHandler();
281                 DnDTabbedPane.this.dragTabIndex = src.indexAtLocation(tabPt.x, tabPt.y);
282                 
283                 // Unhandled NoClassDefFoundError #56620: Could not initialize class java.awt.dnd.DragSource
284                 th.exportAsDrag(src, e, TransferHandler.MOVE);
285 
286                 DnDTabbedPane.RECT_LINE.setBounds(0, 0, 0, 0);
287                 src.getRootPane().getGlassPane().setVisible(true);
288                 src.setDropLocation(new DnDDropLocation(tabPt, -1), true);
289                 
290                 this.startPt = null;
291             }
292         }
293 
294         @Override
295         public void mouseClicked(MouseEvent e) {
296             var tabPt = e.getPoint();
297             JTabbedPane src = (JTabbedPane) e.getSource();
298             
299             int i = src.indexAtLocation(tabPt.x, tabPt.y);
300             if (-1 < i && e.getButton() == MouseEvent.BUTTON2) {
301                 ActionCloseTabResult.perform(i);
302             }
303         }
304     }
305     
306     public final DnDDropLocation getDropLocation() {
307         return this.dropLocation;
308     }
309 }