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) {
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
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) {
233 GhostGlassPane glassPane = (GhostGlassPane) c;
234 glassPane.setTargetTabbedPane(DnDTabbedPane.this);
235 glassPane.repaint();
236 }
237 }
238
239
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
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
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
269
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
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 }