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;
23 private static final int BUTTON_SIZE = 30;
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 jButton) {
60 if (scrollForwardButton == null) {
61 scrollForwardButton = jButton;
62 } else if (scrollBackwardButton == null) {
63 scrollBackwardButton = jButton;
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 dnDDropLocation) {
121 this.dropLocation = dnDDropLocation;
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 jComponent) {
143 jComponent.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
161
162 if (isEnabled) {
163 this.setSelectedIndex(tgtindex);
164 }
165
166
167
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 {
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 glassPane) {
233 glassPane.setTargetTabbedPane(DnDTabbedPane.this);
234 glassPane.repaint();
235 }
236 }
237
238
239 @Override
240 public void propertyChange(PropertyChangeEvent e) {
241 String propertyName = e.getPropertyName();
242 if ("dropLocation".equals(propertyName)) {
243 this.repaintDropLocation();
244 }
245 }
246
247
248 @Override
249 public void mousePressed(MouseEvent mouseEvent) {
250 DnDTabbedPane src = (DnDTabbedPane) mouseEvent.getComponent();
251 boolean isOnlyOneTab = src.getTabCount() <= 1;
252 if (isOnlyOneTab) {
253 this.startPt = null;
254 return;
255 }
256
257 var tabPt = mouseEvent.getPoint();
258 int idx;
259
260 try {
261 idx = src.indexAtLocation(tabPt.x, tabPt.y);
262 } catch (IllegalArgumentException e) {
263 LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
264 return;
265 }
266
267
268
269 boolean flag = idx < 0 || !src.isEnabledAt(idx) || Objects.isNull(src.getComponentAt(idx));
270
271 this.startPt = flag ? null : tabPt;
272 }
273
274 @Override
275 public void mouseDragged(MouseEvent e) {
276 var tabPt = e.getPoint();
277 if (Objects.nonNull(this.startPt) && this.startPt.distance(tabPt) > this.gestureMotionThreshold) {
278 DnDTabbedPane src = (DnDTabbedPane) e.getComponent();
279 var th = src.getTransferHandler();
280 DnDTabbedPane.this.dragTabIndex = src.indexAtLocation(tabPt.x, tabPt.y);
281
282
283 th.exportAsDrag(src, e, TransferHandler.MOVE);
284
285 DnDTabbedPane.RECT_LINE.setBounds(0, 0, 0, 0);
286 src.getRootPane().getGlassPane().setVisible(true);
287 src.setDropLocation(new DnDDropLocation(tabPt, -1), true);
288
289 this.startPt = null;
290 }
291 }
292
293 @Override
294 public void mouseClicked(MouseEvent e) {
295 var tabPt = e.getPoint();
296 JTabbedPane src = (JTabbedPane) e.getSource();
297
298 int i = src.indexAtLocation(tabPt.x, tabPt.y);
299 if (-1 < i && e.getButton() == MouseEvent.BUTTON2) {
300 ActionCloseTabResult.perform(i);
301 }
302 }
303 }
304
305 public final DnDDropLocation getDropLocation() {
306 return this.dropLocation;
307 }
308 }