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