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
22
23 private static final Logger LOGGER = LogManager.getRootLogger();
24
25 private static final int SCROLL_SIZE = 20;
26 private static final int BUTTON_SIZE = 30;
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
164
165 if (isEnabled) {
166 this.setSelectedIndex(tgtindex);
167 }
168
169
170
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 {
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
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
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
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
272
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
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 }